四、实现JWT权限认证
前提准备(目录介绍与依赖引入)
引入依赖:
<!-- 引入jwt依赖 --> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.4.0</version> </dependency>
3.1、JwtUtil以及yml配置
application.yml:主要配置一些token的header类型,token密钥定义
#token配置 token: header: token # header类型 secret: 789 #token的秘钥 expireTime: 1 #token的有效时间,以天为单位,默认为1天
JwtUtil:工具类,对第三方jwt工具类进行封装,主要用于创建、注册JWT以及获取到JWT中的键值对
package com.changlu.springbootdemo.utils; import com.auth0.jwt.JWT; import com.auth0.jwt.JWTCreator; import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.interfaces.DecodedJWT; import com.changlu.springbootdemo.pojo.User; import com.changlu.springbootdemo.pojo.response.LoginUser; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import javax.servlet.http.HttpServletRequest; import java.util.Calendar; import java.util.Collections; import java.util.Date; import java.util.Map; /** * JWT工具类 * * @author changlu * @date 2021/08/09 16:27 **/ @Component @Data @Slf4j public class JwtUtil { // header头部声明类型 @Value("${token.header}") private String header; // signature中的秘钥 @Value("${token.secret}") private String secret; // 过期时间 @Value("${token.expireTime}") private static Integer expireTime; /** * 默认过期时间为1天 */ private static final Integer DEFAULT_EXPIRETIME = 1; /** * 生成JWT token * * @param playLoadMap 封装包含用户信息的map * @return */ public String createToken(Map<String, String> playLoadMap) { // playload主体信息为空则不生成token if (CollectionUtils.isEmpty(playLoadMap)) { return null; } // 过期时间:若是配置文件不配置就使用默认过期时间(1天) Calendar ca = Calendar.getInstance(); if (expireTime == null || expireTime <= 0) { expireTime = DEFAULT_EXPIRETIME; } ca.add(Calendar.DATE, expireTime); // 创建JWT的token对象 JWTCreator.Builder builder = JWT.create(); playLoadMap.forEach((k, v) -> { builder.withClaim(k, v); }); // 设置发布事件 builder.withIssuedAt(new Date()); // 过期时间 builder.withExpiresAt(ca.getTime()); // 签名加密 String token = builder.sign(Algorithm.HMAC256(secret)); return token; } /** * 从token中获取到指定指定keyName的value值 * @param keyName 指定的keyname * @param token token字符串 * @return 对应keyName的value值 */ public String getTokenClaimByName(String keyName,String token){ DecodedJWT decode = JWT.decode(token); return decode.getClaim(keyName).asString(); } /** * 验证JwtToken 不抛出异常说明验证通过 * @param token JwtToken数据 */ public void verifyToken(String token)throws Exception{ JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(secret)).build(); jwtVerifier.verify(token); } /** * 生成响应对象返回给前端 * @param user pojo对象 * @param token token * @param request 请求对象 * @return */ public LoginUser buildLoginUser(User user, String token, HttpServletRequest request){ // 过期的毫秒数 Long expireTimeMillis = expireTime * 24 * 60 * 60 *1000L; // LoginUser作为登陆用户信息实体类(用于返回给前台的相关信息) LoginUser loginUser = new LoginUser(); user.setPassword(null); loginUser.setToken(token);// 登陆凭证 loginUser.setUser(user);// 用户信息 loginUser.setLoginTime(System.currentTimeMillis()); loginUser.setExpireTime(expireTimeMillis); loginUser.setIpAddr(request.getRemoteAddr()); return loginUser; } }
3.2、pojo(User)与vo(UserRequest以及LoginUser)
pojo
User.java:用于描述数据库中的实体类,ORM映射模型
import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; /** * @ClassName User * @Author ChangLu * @Date 2021/7/28 23:15 * @Description TODO */ @Data @AllArgsConstructor @NoArgsConstructor public class User implements Serializable { private Integer id; private String username; private String password; }
vo
UserRequest以及LoginUser:前者是用于接收请求体参数的,后者则是用于返回给前端的响应实体类(描述用户的登录状态)。
UserRequest.java: import com.changlu.springbootdemo.pojo.User; import java.io.Serializable; /** * @ClassName UserReuqest * @Author ChangLu * @Date 2021/8/15 18:29 * @Description TODO */ //直接继承User实体类得到其属性,之后也可以继续进行扩展 public class UserRequest extends User implements Serializable { private static final long serialVersionUID = -7849794470884667710L; }
LoginUser.java:
import com.changlu.springbootdemo.pojo.User; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; /** * @ClassName LoginUser * @Author ChangLu * @Date 2021/8/15 17:53 * @Description TODO */ @Data @AllArgsConstructor @NoArgsConstructor public class LoginUser { // 用户唯一标识 private String token; // 登陆时间 private Long loginTime; // 过期时间 private Long expireTime; // 登陆IP地址 private String ipAddr; // 登录地点 private String loginLocation; // 登陆的用户 private User user; }
3.3、JWT拦截器实现与注册拦截器
JwtInterceptor.java
首先需要实现一个拦截器,其主要目的是对所有的请求进行拦截校验,当一个用户登录好之后得到token,之后的请求都会携带这个token,这个token就是其自身的凭证,在拦截器中对token进行校验:
import com.auth0.jwt.exceptions.AlgorithmMismatchException; import com.auth0.jwt.exceptions.SignatureGenerationException; import com.auth0.jwt.exceptions.TokenExpiredException; import com.changlu.springbootdemo.enums.CommonExceptionEnum; import com.changlu.springbootdemo.exception.OwnException; import com.changlu.springbootdemo.utils.JwtUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * @ClassName JwtInterceptor * @Author ChangLu * @Date 2021/8/15 18:05 * @Description TODO */ @Component @Slf4j public class JwtInterceptor implements HandlerInterceptor { @Autowired private JwtUtil jwtUtil; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 获取token值 String token = request.getHeader("Authorization"); try { //验证token是否有误,若是验证失败会相应抛出指定的异常 jwtUtil.verifyToken(token); return true; }catch (SignatureGenerationException signatureGenerationException){ throw new OwnException(CommonExceptionEnum.TOKEN_VERITY_GENERATED); }catch (TokenExpiredException tokenExpiredException){ throw new OwnException(CommonExceptionEnum.TOKEN_VERITY_EXPIRED); }catch (AlgorithmMismatchException algorithmMismatchException){ throw new OwnException(CommonExceptionEnum.TOKEN_VERITY_ALGORITHM_NOT_MATCH); } } }
//token验证异常枚举实体类 TOKEN_VERITY_GENERATED(3001,"token使用算法签名时无法生成令牌的签名"), TOKEN_VERITY_EXPIRED(3002,"token校验已过期"), TOKEN_VERITY_ALGORITHM_NOT_MATCH(3003,"token校验加密方法无效");
拦截器注册
当我们定义好拦截器以后,就需要将其进行注册到webmvc中,只要注册了之后才会使用该拦截器进行拦截校验。
/** * @ClassName WebSecurityConfig * @Author ChangLu * @Date 2021/8/15 18:24 * @Description TODO */ @Configuration public class WebSecurityConfig implements WebMvcConfigurer { @Autowired private JwtInterceptor jwtInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { //拦截所有请求,除了登陆请求(登陆请求我们不需要进行拦截,其是产生token的源头) registry.addInterceptor(jwtInterceptor).addPathPatterns("/**").excludePathPatterns("/user/login"); } }
3.4、测试token
我们来模拟三次请求来测试该token工具类是否有效:
登陆:测试其是否能够产生token并返回给前台。 删除用户:该请求携带指定token请求头,来看看是否能够通过权限校验。 查询token中的id:该请求是用于获取token字符串的载荷携带的信息,将其token字符串内部的id值进行返回。 import com.changlu.springbootdemo.common.ResultBody; import com.changlu.springbootdemo.enums.CommonExceptionEnum; import com.changlu.springbootdemo.exception.OwnException; import com.changlu.springbootdemo.pojo.User; import com.changlu.springbootdemo.pojo.response.LoginUser; import com.changlu.springbootdemo.pojo.request.UserRequest; import com.changlu.springbootdemo.utils.JwtUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import javax.servlet.http.HttpServletRequest; import java.util.HashMap; import java.util.Map; /** * @ClassName UserController * @Author ChangLu * @Date 2021/7/28 23:14 * @Description TODO */ @RestController @Slf4j @RequestMapping("/user") public class UserController { @Autowired private JwtUtil jwtUtil; /** * 登陆 * * @param request * @param userRequest * @return */ @PostMapping("/login") public ResultBody userLogin(HttpServletRequest request, @RequestBody UserRequest userRequest) { User user = new User(1111, "changlu", "123456"); if (!("changlu".equals(userRequest.getUsername()) && "123456".equals(userRequest.getPassword()))) { // 登陆不成功 throw new OwnException(CommonExceptionEnum.LOGIN_ERROR); } else { // 登陆成功 Map<String, String> playLoadMap = new HashMap<>(1); playLoadMap.put("id", user.getId().toString()); String token = jwtUtil.createToken(playLoadMap); LoginUser loginUser = jwtUtil.buildLoginUser(user, token, request); return ResultBody.success(loginUser); } } /** * 删除指定id用户 * * @param id 指定用户的id * @return */ @DeleteMapping("/{id}") public ResultBody queryList(@PathVariable("id") Integer id) { return ResultBody.success("删除id为" + id + "的用户成功!"); } /** * 查询token中的id * @param request * @return */ @GetMapping("/") public ResultBody queryLoginId(HttpServletRequest request) { String token = request.getHeader("Authorization"); String id = jwtUtil.getTokenClaimByName("id", token); return ResultBody.success("取到token中存储的id值为:" + id); } }
说明:对于之后的两次请求都要携带指定的header键值对,也就是对应的token,可从第一次请求返回值中获取!
五、JWT如何实现注销
浏览器cookie清除(但是服务器还是存在)
建议将时间设置稍微短一点
使用jwt就不需要使用到redis缓存,对于的值需要进行缓存。
JWT最好设置时间为1天或者30分钟。