SpringBoot整合JWT+shiro

2021-05-01 128 次浏览 次点赞

SpringBoot整合JWT+shiro

前段时间写了个前后端分离的项目,被跨域的问题折磨了,前几天学了JWT,和shiro整合后就可以更方便了。

不过自己技术浅薄,在网上找了会后发现一个挺不错的博客,看了一遍后写完了。

放上链接:

https://blog.csdn.net/u010606397/article/details/104110093

1、shiro的工作流程

在整合这两个之前我们需要清楚shiro+jwt的工作流程,如下:

1、AccessControlFilter#onPreHandle()
2、AuthenticationFilter#isAccessAllowed(),此方法仅判断当前的subject(用户)是否已经认证,结果subject.authenticated是未认证。
3、JwtFilter#onAccessDenied(),用户未认证,此方法被调用。
4、JwtFilter#executeLogin(), 使用请求头Authorization生成JwtToken,然后执行subject.login(JwtToken);
5、AuthenticatingRealm#getAuthenticationInfo(),通过JwtToken查询缓存中的AuthenticationInfo
6、缓存中没有AuthenticationInfo,进入 JwtRealm#doGetAuthenticationInfo(),校验token合法性,并生成SimpleAuthenticationInfo。

7、DefaultWebSubjectFactory#createSubject,使用SimpleAuthenticationInfo中的principals生成一个已认证的subject,用户认证就完成了。

授权流程是这样:

1、AccessControlFilter#onPreHandle()
2、PermissionsAuthorizationFilter#isAccessAllowed(),判断subject是否具备接口权限。
3、AuthorizingRealm#getAuthorizationInfo(),获取授权缓存。
4、没有授权缓存,通过JwtRealm#doGetAuthorizationInfo()获取授权信息。

有了这个我们就开始写代码了。

2、JWTUtils

首先需要有一个JWT的工具类,我这个工具类写的很不完善,临时用一下。

public class JWTUtils {
    private static final String SIGN = "!@a$Df^a8b)%";

    public static String getToken(Map<String,String> map){
        Calendar instance = Calendar.getInstance();
        instance.add(Calendar.DATE,7);
        JWTCreator.Builder builder = JWT.create();
        builder.withExpiresAt(instance.getTime());
        for (String s : map.keySet()) {
            builder.withClaim(s,map.get(s));
        }
        String token = builder.sign(Algorithm.HMAC256(SIGN));

        return token;
    }

    public static DecodedJWT verify(String token){
        return JWT.require(Algorithm.HMAC256(SIGN)).build().verify(token);
    }

    /*public static DecodedJWT getTokenInfo(String token){
        return JWT.require(Algorithm.HMAC256(SIGN)).build().verify(token);
    }*/
    public static String getUsername(String token){
        String username = String.valueOf(JWT.require(Algorithm.HMAC256(SIGN)).build().verify(token).getClaim("username"));
        return username;
    }
}

2、JwtToken

创建一个JwtToken用来替代AuthenticationToken,如下:

public class JwtToken implements AuthenticationToken {
    private String token;

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

    @Override
    public Object getPrincipal() {
        return this.token;
    }
    //因为token里面有信息了,这个直接返回空就行
    @Override
    public Object getCredentials() {
        return "";
    }
}

3、Filter

根据上面的流程,我们需要两个filter,一个BasicHttpAuthenticationFilter,一个PermissionsAuthorizationFilter。

首先创建一个JwtFilter,继承BasicHttpAuthenticationFilter,如下:

//创建一个JwtFilter继承BasicHttpAuthenticationFilter
public class JwtFilter extends BasicHttpAuthenticationFilter {

    /*
    重写createToken,onAccessDenied,executeLogin三个方法
     */
    @Override
    protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
        //获取请求头中Authorization的值
        String authorization = getAuthzHeader(request);
        //用获取到的值创建token对象
        return new JwtToken(authorization);
    }

    //身份认证未通过
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        boolean r;
        String authorization = getAuthzHeader(request);
        HashMap<String, Object> map = new HashMap<>();
        //判断authorization是否为空
        if (authorization == null||authorization == ""){
            map.put("state",false);
            map.put("massage","没有token");
            //没有token,返回false,登录页面虽然没有token,但是不会被拦截
            r = false;
            response.setCharacterEncoding("UTF-8");
            response.getWriter().println(JSONObject.toJSON(map));
        } else {
            r = executeLogin(request, response);
        }

        return r;
    }

    //执行登录操作
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        AuthenticationToken token = this.createToken(request, response);
        HashMap<String, Object> map = new HashMap<>();
        response.setCharacterEncoding("UTF-8");
        if(token == null){
            map.put("state",false);
            map.put("massage","未登录");
            return false;
        } else{
            try{
                Subject subject = this.getSubject(request, response);
                subject.login(token);
                return this.onLoginSuccess(token,subject,request,response);
            } catch (Exception e){
                map.put("state",false);
                map.put("massage","用户认证异常");
                e.printStackTrace();
                response.getWriter().println(JSONObject.toJSON(map));
                response.getWriter().close();
                return false;
            }

        }
    }
}

用户访问时,被过滤器拦截下来后因为没有认证,调用onAccessDenied方法,如果没有附带token,就返回没有token,访问结束。如果附带了token,则执行executeLogin方法。

然后创建一个CustomPermissionsAuthorizationFilter继承PermissionsAuthorizationFilter,如下:

public class CustomPermissionsAuthorizationFilter extends PermissionsAuthorizationFilter {

    /**
     * 用户无权访问url时,此方法会被调用
     * 默认实现为org.apache.shiro.web.filter.authz.AuthorizationFilter#onAccessDenied()
     * 覆盖父类的方法,返回自定义信息给前端
     *
     * 接口doc上说:
     *    AuthorizationFilter子类(权限授权过滤器)的onAccessDenied()应该永远返回false,那么在onAccessDenied()内就必然要发送response响应给前端,不然前端就收不到任何数据
     *    AuthenticationFilter、AuthenticatingFilter子类(身份认证过滤器)的onAccessDenied()的返回值则没有限制
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response){
        HashMap<String, Object> map = new HashMap<>();
        map.put("state",false);
        map.put("massage","权限不足");
        try {
            response.getWriter().println(JSONObject.toJSONString(map));
            response.getWriter().close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return false;
    }

}

4、Realm

public class JwtRealm extends AuthorizingRealm {

    //因为是熟悉流程,所以授权就没写
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        return null;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        String token = (String) authenticationToken.getPrincipal();
        try{
            //检查token是否有效,如果有效就创建一个subject
            JWTUtils.verify(token);
            return new SimpleAuthenticationInfo(token, "",this.getName());
        } catch (Exception e){
            e.printStackTrace();
            return null;
        }

    }
    // subject.login(token)方法中的token是JwtToken时,调用此Realm的doGetAuthenticationInfo
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }
}

5、Config

最后需要一个ShiroConfig来配置好上面的过滤器和Realm

@Configuration
public class ShiroConfig {

    @Bean
    public JwtRealm jwtRealm(){
        return new JwtRealm();
    }

    @Bean("mySecurityManager")
    public DefaultWebSecurityManager securityManager(@Qualifier("jwtRealm") JwtRealm jwtRealm){
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        manager.setRealm(jwtRealm);


        //禁止session持久化存储
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        manager.setSubjectDAO(subjectDAO);

        return manager;
    }

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("mySecurityManager") DefaultWebSecurityManager securityManager){
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();

        factoryBean.setSecurityManager(securityManager);

        //添加filter,factoryBean.getFilters()获取到的是引用,可直接添加值
        factoryBean.getFilters().put("jwt", new JwtFilter());
        factoryBean.getFilters().put("customPerms", new CustomPermissionsAuthorizationFilter());

        /**
         * factoryBean.getFilterChainDefinitionMap();默认是size=0的LinkedHashMap
         */
        Map<String, String> definitionMap = factoryBean.getFilterChainDefinitionMap();

        /**
         * definitionMap是一个LinkedHashMap,是一个链表结构
         * put的顺序很重要,当/open/**匹配到请求url后,将不再检查后面的规则
         * 官方将这种原则称为 FIRST MATCH WINS
         * https://waylau.gitbooks.io/apache-shiro-1-2-x-reference/content/III.%20Web%20Applications/10.%20Web.html
         */


        /**
         * 由于禁用了session存储,shiro不会存储用户的认证状态,所以在接口授权之前要先认证用户,不然CustomPermissionsAuthorizationFilter不知道用户是谁
         * 实际项目中可以将这些接口权限规则放到数据库中去
         */
        /*definitionMap.put("/role/permission/edit", "jwt, customPerms["+userService.getRolePermisssionEdit().getName()+"]");*/

        // 前面的规则都没匹配到,最后添加一条规则,所有的接口都要经过com.example.shirojwt.filter.JwtFilter这个过滤器验证
        definitionMap.put("/login", "anon");
        definitionMap.put("/**", "jwt");

        factoryBean.setFilterChainDefinitionMap(definitionMap);

        return factoryBean;

    }
}

这次的学习让我对shiro有了更深刻的理解,不再是像以前一样连用都用不太好。所以说经常看代码还是很有用的。


本文由 Lemuria 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。

还不快抢沙发

添加新评论