接入Shiro(1)——极简登录认证

Shiro是一个强大易用的Java安全框架,提供了认证、授权、加密和会话管理等功能。之前的开发都是在低代码上直接使用Shiro,一般也不需要修改。

在SpringBoot的基础上,只接入Shiro的登录认证还是头一次。看各方文档和代码,不是一大坨概念讲的云里雾里,就是一大坨代码看的眼花缭乱。认证和授权拌在一起,咽又咽不下去,咽了一点也消化不了。

花了一天时间终于从JeecgBoot的代码中拆出了登录认证,又花了一天时间做了个极简版本,现在分享给大家。

一、背景

后端基于SpringBoot,前端基于vue使用antdv的组件,即前后端分离。

二、准备

1、添加Shiro依赖


	org.apache.shiro
	shiro-spring
	1.9.0

2、前端请求

为了简化前端,直接使用Swagger2的API接口文档做前端测试。

Swagger2的接入,请参考之前的博文,接入knife4(3.0.3)。

3、后端接口

后端添加俩测试用的接口,一个接口不需要认证,另一个接口需要认证,代码如下:

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Api(tags = "测试Shiro")
@RestController
@RequestMapping("/demo")
public class DemoRC {

    /**
     * 这个接口不需要认证
     */
    @ApiOperation("Get1")
    @GetMapping("/get1")
    String get() {
        return "get1";
    }

    /**
     * 这个接口需要认证
     */
    @ApiOperation("Get2")
    @GetMapping("/get2")
    String get2() {
        return "get2";
    }
}

三、接入Shiro

1、添加Shiro配置

配置是过滤器的第一步,需要告诉框架,Shiro要处理什么样的网络请求,代码如下:

import com.example.demo.config.shiro.filters.CustomFilter;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

@Configuration
public class ShiroConfig {

    @Bean
    public ShiroFilterFactoryBean shirFilter() {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager());

        // Swagger相关地址放入白名单
        Map filterChainDefinitionMap = new LinkedHashMap<>();
        filterChainDefinitionMap.put("/doc.html", "anon");
        filterChainDefinitionMap.put("/swagger**/**", "anon");
        filterChainDefinitionMap.put("/webjars/**", "anon");
        filterChainDefinitionMap.put("/v3/**", "anon");

        // 测试接口放入白名单(不需要认证)
        filterChainDefinitionMap.put("/demo/get1", "anon");

        // 白名单之外的,都得通过该过滤器
        Map filterMap = new HashMap(1);
        filterMap.put("custom", new CustomFilter());
        shiroFilterFactoryBean.setFilters(filterMap);
        filterChainDefinitionMap.put("/**", "custom");

        // 设置过滤器
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

    /**
     * 创建默认安全管理对象
     */
    private DefaultWebSecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 设置自己的认证员(只有自己知道token与用户名的关系)
        securityManager.setRealm(new CustomRealm());
        return securityManager;
    }
}

其中CustomFilter和CustomRealm是自定义类,下面来定义这俩类。

2、过滤器CustomFilter

import com.example.demo.config.shiro.CustomToken;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class CustomFilter extends BasicHttpAuthenticationFilter {

    /**
     * 是否允许通过,只做最基本的判断
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String token = httpServletRequest.getHeader("ACCESS-TOKEN");
        // Token存在,且长度必须大于6
        if (token != null && token.length() > 6) {
            CustomToken customToken = new CustomToken(token);
            // 进一步认证失败,则不允许通过
            try {
                getSubject(request, response).login(customToken);
            } catch (AuthenticationException e) {
                return false;
            }
            return true;
        }
        return false;
    }

    /**
     * 没通过判断,则返回固定的数据
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
        HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
        httpServletResponse.setCharacterEncoding("UTF-8");
        httpServletResponse.setContentType("application/json");
        httpServletResponse.getWriter().write("{\"code\":403,\"message\":\"用户未登录,请进行登录\"}");
        return false;
    }

    /**
     * 对跨域提供支持
     */
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
        HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
        {
            httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
            httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
            httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
            // 是否允许发送Cookie,默认Cookie不包括在CORS请求之中。设为true时,表示服务器允许Cookie包含在请求中。
            httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");
        }
        // 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }
}

如果是前后端不分离的,不需要实现方法preHandle。

这里要求前端请求的Header中包含“ACCESS-TOKEN”,并且值的长度大于6,根据需要自己修改。

3、CustomToken

自定义一个Token类,以便做进一步的认证,代码如下:

import org.apache.shiro.authc.AuthenticationToken;

public class CustomToken implements AuthenticationToken {
    private final String token;

    public CustomToken(String token) {
        this.token = token;
    }

    @Override
    public Object getPrincipal() {
        return this.token;
    }

    @Override
    public Object getCredentials() {
        return this.token;
    }

    public String getToken() {
        return this.token;
    }
}

4、进一步认证CustomRealm

import org.apache.shiro.authc.*;
import org.apache.shiro.realm.AuthenticatingRealm;

public class CustomRealm extends AuthenticatingRealm {

    /**
     * 告诉框架,CustomToken类型的Token必须要通过当前类的认证
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof CustomToken;
    }

    /**
     * 登录认证
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
        CustomToken customToken = (CustomToken)auth;
        // token长度为8才是合法的
        if (customToken.getToken().length() != 8) {
            throw new AuthenticationException("token错误!");
        }
        // 从token中得到用户名
        String username = customToken.getToken().substring(0, 4);
        // 根据用户名得到用户信息
        Object userInfo = new String(username);
        return new SimpleAuthenticationInfo(userInfo, customToken.getToken(), getName());
    }
}

四、测试

1、开启动态参数请求,以便修改Header

打开API接口文档,点击菜单“文档管理” -> “个性化设置”,勾选“开启动态请求参数”,然后刷新页面。如下图:

接入Shiro(1)——极简登录认证_第1张图片

 2、测试

(1)不带Header

点击菜单“测试Shiro”下的“Get1”和“Get2”,分别请求,可以发现Get1能正常请求到数据,Get2则返回了403的固定数据(CustomFilter中写死的)。如下图:

接入Shiro(1)——极简登录认证_第2张图片

   (2)token太短

Get2,添加请求头。请求头名称“ACCESS-TOKEN”,请求头内容“123456”。依然返回403,因为CustomFilter中要求token长度必须大于6,基本判断没通过。如下图:

接入Shiro(1)——极简登录认证_第3张图片

  (3)token位数错误

请求头内容输入“1234567”,依然返回403。CustomRealm中判断token不是8位抛了异常,但CustomFilter中捕捉了该异常。如下图:

接入Shiro(1)——极简登录认证_第4张图片

 (4)认证通过

请求头中输入“12345678”,正确得到了返回结果。如下图:

接入Shiro(1)——极简登录认证_第5张图片

五、结束

终于,终于弄通了Shiro认证的基本流程,下一步处理登录逻辑生成token,然后进行token认证。

你可能感兴趣的