Learn Spring Boot: (16) Use Shiro and JWT to implement authentication services

foreword

Web applications need to be made stateless, that is, server-side stateless, which means that the server-side will not store things like sessions, but access_token for resource access each time a request is made. Here we'll use JWT 1 , a hash-based message authentication code, using a key and a message as input, to generate their message digests. This key is only known to the server. The message digest is used for dissemination when accessing, and the server then verifies the message digest.

Authentication steps

  1. The client uses the username and password to access the authentication server for the first time, the server verifies the username and password, the authentication is successful, and the JWT is generated using the user key and returned
  2. After that, each time the client requests the client to bring the JWT
  3. The server validates the JWT

custom jwt interceptor

/**
 * oauth2拦截器,现在改为 JWT 认证
 */
public class OAuth2Filter extends FormAuthenticationFilter {
    /**
     * 设置 request 的键,用来保存 认证的 userID,
     */
    private final static String USER_ID = "USER_ID";
    @Resource
    private JwtUtils jwtUtils;

    /**
     * logger
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(OAuth2Filter.class);


    /**
     * shiro权限拦截核心方法 返回true允许访问resource,
     *
     * @param request
     * @param response
     * @param mappedValue
     * @return
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        String token = getRequestToken((HttpServletRequest) request);
        try {
            // 检查 token 有效性
            //ExpiredJwtException JWT已过期
            //SignatureException JWT可能被篡改
            Jwts.parser().setSigningKey(jwtUtils.getSecret()).parseClaimsJws(token).getBody();
        } catch (Exception e) {
            // 身份验证失败,返回 false 将进入onAccessDenied 判断是否登陆。
            onLoginFail(response);
            return false;
        }
        Long userId = getUserIdFromToken(token);
        // 存入到 request 中,在后面的业务处理中可以使用
        request.setAttribute(USER_ID, userId);
        return true;
    }

    /**
     * 当访问拒绝时是否已经处理了;
     * 如果返回true表示需要继续处理;
     * 如果返回false表示该拦截器实例已经处理完成了,将直接返回即可。
     *
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        if (isLoginRequest(request, response)) {
            if (isLoginSubmission(request, response)) {
                return executeLogin(request, response);
            } else {
                return true;
            }
        } else {
            onLoginFail(response);
            return false;
        }
    }

    /**
     * 鉴定失败,返回错误信息
     * @param token
     * @param e
     * @param request
     * @param response
     * @return
     */
    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
        try {
            ((HttpServletResponse) response).setStatus(HttpStatus.BAD_REQUEST.value());
            response.getWriter().print("账号活密码错误");
        } catch (IOException e1) {
            LOGGER.error(e1.getMessage(), e1);
        }
        return false;
    }

    /**
     * token 认证失败
     *
     * @param response
     */
    private void onLoginFail(ServletResponse response) {
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        ((HttpServletResponse) response).setStatus(HttpStatus.UNAUTHORIZED.value());
        try {
            response.getWriter().print("没有权限,请联系管理员授权");
        } catch (IOException e) {
            LOGGER.error(e.getMessage(), e);
        }
    }

    /**
     * 获取请求的token
     */
    private String getRequestToken(HttpServletRequest httpRequest) {
        //从header中获取token
        String token = httpRequest.getHeader(jwtUtils.getHeader());
        //如果header中不存在token,则从参数中获取token
        if (StringUtils.isBlank(token)) {
            return httpRequest.getParameter(jwtUtils.getHeader());
        }
        if (StringUtils.isBlank(token)) {
            // 从 cookie 获取 token
            Cookie[] cookies = httpRequest.getCookies();
            if (null == cookies || cookies.length == 0) {
                return null;
            }
            for (Cookie cookie : cookies) {
                if (cookie.getName().equals(jwtUtils.getHeader())) {
                    token = cookie.getValue();
                    break;
                }
            }
        }
        return token;
    }

    /**
     * 根据 token 获取 userID
     *
     * @param token token
     * @return userId
     */
    private Long getUserIdFromToken(String token) {
        if (StringUtils.isBlank(token)) {
            throw new KCException("无效 token", HttpStatus.UNAUTHORIZED.value());
        }
        Claims claims = jwtUtils.getClaimByToken(token);
        if (claims == null || jwtUtils.isTokenExpired(claims.getExpiration())) {
            throw new KCException(jwtUtils.getHeader() + "失效,请重新登录", HttpStatus.UNAUTHORIZED.value());
        }
        return Long.parseLong(claims.getSubject());
    }

}

Set the custom shiro interceptor to ShiroFilterFactoryBean, and then pathset the interception filter that needs to be authenticated.

login

    @PostMapping("/login")
    @ApiOperation("系统登陆")
    public ResponseEntity<String> login(@RequestBody SysUserLoginForm userForm) {
        String kaptcha = ShiroUtils.getKaptcha(Constants.KAPTCHA_SESSION_KEY);
        if (!userForm.getCaptcha().equalsIgnoreCase(kaptcha)) {
            throw new KCException("验证码不正确!");
        }
        UsernamePasswordToken token = new UsernamePasswordToken(userForm.getUsername(), userForm.getPassword());
        Subject currentUser = SecurityUtils.getSubject();
        currentUser.login(token);

        //账号锁定
        if (getUser().getStatus() == SysConstant.SysUserStatus.LOCK) {
            throw new KCException("账号已被锁定,请联系管理员");
        }
        // 登陆成功后直接返回 token ,然后后续放到 header 中认证
        return ResponseEntity.status(HttpStatus.OK).body(jwtUtils.generateToken(getUserId()));
    }

JwtUtils

I set three parameters for jwt earlier

# jwt 配置
jwt:
  # 加密密钥
  secret: 61D73234C4F93E03074D74D74D1E39D9 #blog.wuwii.com
  # token有效时长
  expire: 7 # 7天,单位天
  # token 存在 header 中的参数
  header: token

Writing the jwt tool class

@ConfigurationProperties(prefix = "jwt")
@Component
public class JwtUtils {
    /**
     * logger
     */
    private Logger logger = LoggerFactory.getLogger(JwtUtils.class);

    /**
     * 密钥
     */
    private String secret;
    /**
     * 有效期限
     */
    private int expire;
    /**
     * 存储 token
     */
    private String header;

    /**
     * 生成jwt token
     *
     * @param userId 用户ID
     * @return token
     */
    public String generateToken(long userId) {
        Date nowDate = new Date();

        return Jwts.builder()
                .setHeaderParam("typ", "JWT")
                // 后续获取 subject 是 userid
                .setSubject(userId + "")
                .setIssuedAt(nowDate)
                .setExpiration(DateUtils.addDays(nowDate, expire))
                // 这里我采用的是 HS512 算法
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    /**
     * 解析 token,
     * 利用 jjwt 提供的parser传入秘钥,
     *
     * @param token token
     * @return 数据声明 Map<String, Object>
     */
    public Claims getClaimByToken(String token) {
        try {
            return Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * token是否过期
     *
     * @return true:过期
     */
    public boolean isTokenExpired(Date expiration) {
        return expiration.before(new Date());
    }

    public String getSecret() {
        return secret;
    }

    public void setSecret(String secret) {
        this.secret = secret;
    }

    public int getExpire() {
        return expire;
    }

    public void setExpire(int expire) {
        this.expire = expire;
    }

    public String getHeader() {
        return header;
    }

    public void setHeader(String header) {
        this.header = header;
    }
}

Summarize

Due to the JWT method, the server does not need to save any state, so the server does not need to use session to save user information, and unit testing is more convenient. Although intermediate transcoding and decoding will consume some performance, it has little impact and is more convenient. Applied in SSO 2 .


  1. JSON WEB Token
  2. Single Sign On

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325632465&siteId=291194637