spring boot2整合shiro安全框架实现前后端分离的JWT token登录验证

代码略多,粘贴一些关键的代码,完整demo当然必须放在GitHub上面啦,当然带SQL文件的,在项目里面

GitHub地址:  https://github.com/zhang-xiaoxiang/shiro-jwt

说明:由于初衷是解决自己项目的bug的,就找的网上的一面博客瞎搞了一个demo.然后报的错网上难以找到解决办法,后来自己解决了,就记录一下,所以不算教程,我看评论大伙说的修改密码后之前的token仍然有效,可以使用redis处理,或者其他业务逻辑代码处理一下,这个仅仅是一个demo,当然很多培训机构的源码也有一些demo,大家可以参考,这里主要是给需要的小伙伴一个思路或者遇到那个bug的解决方案

关于这个例子介绍:

1使用的是spring boot2作为环境基础

2数据库框架是mybatis plus(对mybatis熟悉就行,你顺便看一下mybatis plus逆向生成代码的魅力)

3包含全局异常的处理(报错getWriter() has already been called for this response),这个BUG是网上的访问量比较高的CSDN的博客案例留下的bug,为了解决这个bug才有我的这篇博客,所以记录一下

4shiro可以实现除了验证和授权之外的一些功能,这里主要展示验证,验证相当于登录,授权相当于登录后查看是否有权限,就想即使登录了支付宝,但是付款的时候还是需要验证支付宝密码一样,当然前后端未分离可以使用thymeleaf网页引擎,貌似黑马视频有,这里就只做个登录例子

5jwt这种方式是不太时尚的,正确的方式spring security oauth2实现认证授权,这里仅供参考,欢迎小伙伴提出宝贵意见,我都会尽快回复

6使用的是mysql8,pom和properties文件你改成现在用得多一点的5.7版本的写法就行了

7这里根据博友评论确实忘了用户修改密码token的处理,我相信这个大家应该会处理的(更新一下token就行了)

主要步骤:

首先是pom引入(主要的依赖,由于代码较大,而且我提供了Git地址,需要的直接从GitHub拉下来就行了,后面不做过多阐述)

     <!--JWT java web token 权限验证-->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.4.0</version>
        </dependency>
            <!--shiro 权限框架-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.0</version>
        </dependency>
 

实现shiro的AuthenticationToken接口的类JwtToken

//package com.example.shirojwt.jwt;
 
import org.apache.shiro.authc.AuthenticationToken;
 
/**
 * JwtToken:实现shiro的AuthenticationToken接口的类JwtToken
 *
 * @author zhangxiaoxiang
 * @date: 2019/07/12
 */
public class JwtToken implements AuthenticationToken{
 
    private String token;
 
    public JwtToken(String token) {
        this.token = token;
    }
 
    @Override
    public Object getPrincipal() {
        return token;
    }
 
    @Override
    public Object getCredentials() {
        return token;
    }
}

 再写一个token的验证工具类


//package com.example.shirojwt.util;
 
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.DecodedJWT;
import lombok.extern.slf4j.Slf4j;
 
import java.util.Date;
 
/**
 * JwtUtil:用来进行签名和效验Token
 *
 * @author zhangxiaoxiang
 * @date: 2019/07/12
 */
@Slf4j
public class JwtUtil {
    /**
     * JWT验证过期时间 EXPIRE_TIME 分钟
     */
    private static final long EXPIRE_TIME = 30 * 60 * 1000;
 
    /**
     * 校验token是否正确
     *
     * @param token  密钥
     * @param secret 用户的密码
     * @return 是否正确
     */
    public static boolean verify(String token, String username, String secret) {
        try {
            //根据密码生成JWT效验器
            Algorithm algorithm = Algorithm.HMAC256(secret);
            JWTVerifier verifier = JWT.require(algorithm)
                    .withClaim("username", username)
                    .build();
            //效验TOKEN
            DecodedJWT jwt = verifier.verify(token);
            log.info("登录验证成功!");
            return true;
        } catch (Exception exception) {
            log.error("JwtUtil登录验证失败!");
 
            return false;
        }
    }
 
    /**
     * 获得token中的信息无需secret解密也能获得
     *
     * @return token中包含的用户名
     */
    public static String getUsername(String token) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim("username").asString();
        } catch (JWTDecodeException e) {
            return null;
        }
    }
 
    /**
     * 生成token签名EXPIRE_TIME 分钟后过期
     *
     * @param username 用户名(电话号码)
     * @param secret   用户的密码
     * @return 加密的token
     */
    public static String sign(String username, String secret) {
        Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
        Algorithm algorithm = Algorithm.HMAC256(secret);
        // 附带username信息
        return JWT.create()
                .withClaim("username", username)
                .withExpiresAt(date)
                .sign(algorithm);
 
    }
 
    public static void main(String[] args) {
        /**
         * 测试生成一个token
         */
        String sign = sign("18888888888", "123456");
       log.warn("测试生成一个token\n"+sign);
    }
}

写一个jwt过滤器来作为shiro的过滤器。

//package com.example.shirojwt.filter;
 
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.example.shirojwt.jwt.JwtToken;
import com.example.shirojwt.result.ResponseData;
import com.example.shirojwt.result.ResponseDataUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMethod;
 
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
 
 
/**
 * JwtFilter:jwt过滤器来作为shiro的过滤器
 *
 * @author zhangxiaoxiang
 * @date: 2019/07/12
 */
@Slf4j
@Component//这个注入与否影响不大
public class JwtFilter extends BasicHttpAuthenticationFilter implements Filter {
    /**
     * 执行登录
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String token = httpServletRequest.getHeader("Token");
        JwtToken jwtToken = new JwtToken(token);
        // 提交给realm进行登入,如果错误他会抛出异常并被捕获
        try {
            getSubject(request, response).login(jwtToken);
            // 如果没有抛出异常则代表登入成功,返回true
            return true;
        } catch (AuthenticationException e) {
            ResponseData responseData = ResponseDataUtil.authorizationFailed( "没有访问权限,原因是:" + e.getMessage());
            //SerializerFeature.WriteMapNullValue为了null属性也输出json的键值对
            Object o = JSONObject.toJSONString(responseData, SerializerFeature.WriteMapNullValue);
            response.setCharacterEncoding("utf-8");
            response.getWriter().print(o);
            return false;
        }
 
    }
 
    /**
     * 执行登录认证
     *
     * @param request
     * @param response
     * @param mappedValue
     * @return
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        try {
            return executeLogin(request, response);
            // return true;有一篇博客这里直接返回true是不正确的,在这里我特别指出一下
        } catch (Exception e) {
            log.error("JwtFilter过滤验证失败!");
            return false;
        }
    }
 
 
    /**
     * 对跨域提供支持
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) 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"));
        // 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }
}

自定义Realm


//package com.example.shirojwt.shiro;
 
import com.example.shirojwt.entity.User;
import com.example.shirojwt.jwt.JwtToken;
import com.example.shirojwt.service.UserService;
import com.example.shirojwt.util.JwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
 
/**
 * MyRealm:自定义一个授权
 *
 * @author zhangxiaoxiang
 * @date: 2019/07/12
 */
 
@Component
@Slf4j
public class MyRealm extends AuthorizingRealm {
 
    @Autowired
    private UserService userService;
 
    /**
     * 必须重写此方法,不然Shiro会报错
     * @param token
     * @return
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }
 
    /**
     * 只有当需要检测用户权限的时候才会调用此方法,例如checkRole,checkPermission之类的
     * @param principals
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String username = JwtUtil.getUsername(principals.toString());
        User user = userService.findByUserName(username);
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        return simpleAuthorizationInfo;
    }
 
    /**
     * 默认使用此方法进行用户名正确与否验证,错误抛出异常即可。
     * @param auth
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
        String token = (String) auth.getCredentials();
        // 解密获得username,用于和数据库进行对比
        String username = null;
        try {
            //这里工具类没有处理空指针等异常这里处理一下(这里处理科学一些)
            username = JwtUtil.getUsername(token);
        } catch (Exception e) {
            throw new AuthenticationException("heard的token拼写错误或者值为空");
        }
        if (username == null) {
          log.error("token无效(空''或者null都不行!)");
            throw new AuthenticationException("token无效");
        }
        User userBean = userService.findByUserName(username);
        if (userBean == null) {
            log.error("用户不存在!)");
            throw new AuthenticationException("用户不存在!");
        }
        if (!JwtUtil.verify(token, username, userBean.getUserPassword())) {
            log.error("用户名或密码错误(token无效或者与登录者不匹配)!)");
            throw new AuthenticationException("用户名或密码错误(token无效或者与登录者不匹配)!");
        }
        return new SimpleAuthenticationInfo(token, token, "my_realm");
    }
}

ShiroConfig核心配置

//package com.example.shirojwt.config;
 
 
import com.example.shirojwt.filter.JwtFilter;
import com.example.shirojwt.shiro.MyRealm;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.mgt.SecurityManager;
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;
 
/**
 * ShiroConfig:shiro 配置类,配置哪些拦截,哪些不拦截,哪些授权等等各种配置都在这里
 * 
 * 很多都是老套路,按照这个套路配置就行了
 *
 * @author zhangxiaoxiang
 * @date: 2019/07/12
 */
 
@Configuration
public class ShiroConfig {
    /**
     * 注入安全过滤器
     * @param securityManager
     * @return
     */
    @Bean("shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        //拦截器
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
        // 配置不会被拦截的链接 顺序判断
        filterChainDefinitionMap.put("/login/**", "anon");
        filterChainDefinitionMap.put("/**.js", "anon");
        filterChainDefinitionMap.put("/druid/**", "anon");
        filterChainDefinitionMap.put("/swagger**/**", "anon");
        filterChainDefinitionMap.put("/**/swagger**/**", "anon");
        filterChainDefinitionMap.put("/webjars/**", "anon");
        filterChainDefinitionMap.put("/v2/**", "anon");
        // 添加自己的过滤器并且取名为jwt
        Map<String, Filter> filterMap = new HashMap<String, Filter>(1);
        filterMap.put("jwt", new JwtFilter());
        shiroFilterFactoryBean.setFilters(filterMap);
        //<!-- 过滤链定义,从上向下顺序执行,一般将/**放在最为下边
        filterChainDefinitionMap.put("/**", "jwt");
        //未授权界面;
        shiroFilterFactoryBean.setUnauthorizedUrl("/403");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }
 
    /**
     * 注入安全管理器
     * @param myRealm
     * @return
     */
    @Bean("securityManager")
    public SecurityManager securityManager(MyRealm myRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(myRealm);
 
        /*
         * 关闭shiro自带的session,详情见文档
         * http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29
         */
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        securityManager.setSubjectDAO(subjectDAO);
        return securityManager;
 
       }
}

大体核心代码已经完成,项目结构如图(点击图片放大后就清晰了)

测试示例

比如模拟登录token(在JwtUtil生成一个)错误,或者token输入成token12,或者账号密码错误等等…

 比如输入使用生成的token(相当于用户登录后返回的token):登录显然不需要token,登录后后台返回token,只是这里为了演示一个需要token的接口,所以把登录接口也设置了需要token,科学的方法是比如查询用户详情这个接口就必须要token,所以postman传入的token模拟的是需要token的接口而存在的,返回的token是给前端带上访问用户订单,地址的那些需要token认证的接口

当然在控制层也可以测试其他被限制访问的接口和放行的接口,以及各种异常的测试,这里不做过多赘述

发表评论

电子邮件地址不会被公开。 必填项已用*标注