SpringBoot整合JJWT实现Token登录认证
如果你还不知道什么是JWT,可以参考文章:
我们这里用到SpringBoot整合JJWT
实现Token登录认证,JJWT是指Java JWT
,适用于 Java 和 Android 的 JSON Web Token(JWT)库。下面我们通过SpringBoot整合JJWT来实现Token登录认证。
1、为了方便演示,我这里只创建了只有Spring Web
模块的SpringBoot
项目,没有涉及数据库操作,项目创建很简单,我在此只贴两张IDEA中创建的两张图:
只选择Spring Web模块
2、在resources
目录中新建application.yml
配置文件(去除自动生成的application.properties),只简单配置下服务器端口为8080:
server: port: 8080
1、在application.yml
新增JWT相关配置,如下:
# jwt 配置 custom: jwt: # header:凭证(校验的变量名) header: Authorization # 有效期1天(单位:s) expire: 5184000 # secret: 秘钥(普通字符串) 不能太短,太短可能会导致报错 secret: 99c2918fe19d30bce25abfac8a3733ec # 签发者 issuer: panziye
2、在pom.xml
中引入jjwt
相关依赖(我们使用目前最新版0.11.2版本)和fastjson
依赖,方便后面操作json,新增配置如下:
<!-- fastjson --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.9</version> </dependency> <!-- jjwt --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.2</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.11.2</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred --> <version>0.11.2</version> <scope>runtime</scope> </dependency>
3、在项目中新建util
包,在util
包下新建JwtUtil
类,主要实现创建Token、解析Token和判断Token是否过期功能代码如下:
package com.panziye.jwtdemo.util; import io.jsonwebtoken.*; import io.jsonwebtoken.security.Keys; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import javax.crypto.SecretKey; import java.nio.charset.StandardCharsets; import java.util.Date; import java.util.HashMap; import java.util.Map; @Component @ConfigurationProperties(prefix = "custom.jwt") public class JwtUtil { private static final Logger logger = LoggerFactory.getLogger(JwtUtil.class); // 秘钥 private String secret; // 有效时间 private Long expire; // 用户凭证 private String header; // 签发者 private String issuer; /** * 生成token签名 * @param subject * @return */ public String createToken(String subject) { Date now = new Date(); // 过期时间 Date expireDate = new Date(now.getTime() + expire * 1000); //创建Signature SecretKey final SecretKey key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); //header参数 final Map<String, Object> headerMap = new HashMap<>(); headerMap.put("alg", "HS256"); headerMap.put("typ", "JWT"); //生成token String token = Jwts.builder() .setHeader(headerMap) .setSubject(subject) .setIssuedAt(now) .setExpiration(expireDate) .setIssuer(issuer) .signWith(key,SignatureAlgorithm.HS256) .compact(); logger.info("JWT[" + token + "]"); return token; } /** * 解析token * * @param token token * @return */ public Claims parseToken(String token) { Claims claims = null; try { //创建Signature SecretKey final SecretKey key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); claims = Jwts.parserBuilder() .setSigningKey(key) .build() .parseClaimsJws(token) .getBody(); logger.info("Parse JWT token success"); } catch (JwtException e) { logger.info("Parse JWT errror " + e.getMessage()); return null; } return claims; } /** * 判断token是否过期 * * @param expiration * @return */ public boolean isExpired(Date expiration) { return expiration.before(new Date()); } //getter and setter public void setSecret(String secret) { this.secret = secret; } public void setExpire(Long expire) { this.expire = expire; } public void setHeader(String header) { this.header = header; } //用于其他地方获取Header配置信息 public String getHeader() { return header; } public void setIssuer(String issuer) { this.issuer = issuer; } }
4、在项目中新建annotation
包,在annotation
下新建PassLogin
注解类,方便用于后面针对某些请求方法添加此注解可以忽略Token验证,PassLogin.java
代码如下:
package com.panziye.jwtdemo.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface PassLogin { boolean required() default true; }
5、在项目中新建http
包,在http
下新建HttpEnum
枚举类,用于存放响应状态码和对应的提示信息,这里面我们只列出一些常用的,可以根据自己需要补充,代码如下:
package com.panziye.jwtdemo.http; public enum HttpEnum { /** * 请求处理正常 */ OK(200, "请求成功"), /** * 请求成功并且服务器创建了新的资源。 */ CREATED(201, "创建成功"), /** * 用户发出的请求有错误,服务器没有进行新建或修改数据的操作 */ INVALID_REQUEST(400, "非法请求"), /** * 访问内容不存在 */ NOTFOUND(404, "访问内容不存在"), /** * 表示用户没有权限(令牌、用户名、密码错误) */ UNAUTHORIZED(401,"抱歉,您没有权限"), /** * 表示用户得到授权(与401错误相对),但是访问是被禁止的 */ FORBIDDEN(403,"禁止访问"), /** * 系统内部错误 */ INTERNAL_SERVER_ERROR(500, "系统内部错误"); private String msg; private int code; private HttpEnum(int code, String msg) { this.msg = msg; this.code = code; } //获取code public int code(){ return code; } //获取msg public String msg(){ return msg; } }
6、在http
下新建ResponseResult
类,用于封装响应状态码、提示信息和数据,这里只构建了一些常用的实例,可以根据自己的需求扩展,代码如下:
package com.panziye.jwtdemo.http; import java.io.Serializable; public class ResponseResult<T> implements Serializable { private static final long serialVersionUID = 1L; //响应编码 private int code; //提示信息 private String msg; //响应数据 private T data; //根据属性构建ResponseResult private static <T> ResponseResult<T> build( int code,String msg,T data) { return new ResponseResult<T>().setCode(code).setMsg(msg).setData(data); } //构建一些常用的 //请求正常 public static <T> ResponseResult<T> ok(){ return build(HttpEnum.OK.code(),HttpEnum.OK.msg(),null); } public static <T> ResponseResult<T> ok(T data){ return build(HttpEnum.OK.code(),HttpEnum.OK.msg(),data); } //创建成功 public static <T> ResponseResult<T> created(){ return build(HttpEnum.CREATED.code(),HttpEnum.CREATED.msg(),null); } //非法请求 public static <T> ResponseResult<T> invalid_request(){ return build(HttpEnum.INVALID_REQUEST.code(),HttpEnum.INVALID_REQUEST.msg(),null); } //访问内容不存在 public static <T> ResponseResult<T> notFound(){ return build(HttpEnum.NOTFOUND.code(),HttpEnum.NOTFOUND.msg(),null); } //没有权限 public static <T> ResponseResult<T> unauthorized(){ return build(HttpEnum.UNAUTHORIZED.code(),HttpEnum.UNAUTHORIZED.msg(),null); } //禁止访问 public static <T> ResponseResult<T> forbidden(){ return build(HttpEnum.FORBIDDEN.code(),HttpEnum.FORBIDDEN.msg(),null); } //系统内部错误 public static <T> ResponseResult<T> internal_server_error(){ return build(HttpEnum.INTERNAL_SERVER_ERROR.code(),HttpEnum.INTERNAL_SERVER_ERROR.msg(),null); } //getter和setter public int getCode() { return code; } //注意返回值类型 public ResponseResult<T> setCode(int code) { this.code = code; return this; } public String getMsg() { return msg; } //注意返回值类型 public ResponseResult<T> setMsg(String msg) { this.msg = msg; return this; } public T getData() { return data; } //注意返回值类型 public ResponseResult<T> setData(T data) { this.data = data; return this; } }
7、在项目中新建interceptor
包,在interceptor
下新建TokenInterceptor
拦截器类,用于在请求被处理之前对Token
进行拦截验证,代码如下:
package com.panziye.jwtdemo.interceptor; import com.alibaba.fastjson.JSONObject; import com.panziye.jwtdemo.annotation.PassLogin; import com.panziye.jwtdemo.http.ResponseResult; import com.panziye.jwtdemo.util.JwtUtil; import io.jsonwebtoken.Claims; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.util.StringUtils; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.lang.reflect.Method; public class TokenInterceptor implements HandlerInterceptor { private static final Logger logger = LoggerFactory.getLogger(JwtUtil.class); @Autowired private JwtUtil jwtUtil; //在业务处理请求之前处理 @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //设置response响应数据类型为json和编码为utf-8 response.setContentType("application/json;charset=utf-8"); // 判断对象是否是映射到一个方法,如果不是则直接通过 if (!(handler instanceof HandlerMethod)) { // instanceof运算符是用来在运行时指出对象是否是特定类的一个实例 return true; } HandlerMethod handlerMethod = (HandlerMethod) handler; Method method = handlerMethod.getMethod(); //检查方法是否有PassLogin注解,有则跳过认证 if (method.isAnnotationPresent(PassLogin.class)){ return true; } // 从HTTP请求头中获取Authorization信息, String authorization = request.getHeader(jwtUtil.getHeader()); //判断Authorization是否为空 if(StringUtils.isEmpty(authorization)){ logger.info("token无效"); response.getWriter().write(JSONObject.toJSONString(ResponseResult.unauthorized())); return false; } //获取TOKEN,注意要清除前缀"Bearer " String token = authorization.replace("Bearer ",""); // HTTP请求头中TOKEN解析出的用户信息 Claims claims = jwtUtil.parseToken(token); if(claims == null){ logger.info("token无效"); response.getWriter().write(JSONObject.toJSONString(ResponseResult.unauthorized())); return false; } //校验是否过期 boolean flag = jwtUtil.isExpired(claims.getExpiration()); if(flag){ logger.error("token过期"); response.getWriter().write(JSONObject.toJSONString(ResponseResult.unauthorized())); return false; } //token正常,获取用户信息,比如这里的subject存的是用户id String subject = claims.getSubject(); //将用户信息存入request,以便后面处理请求使用 request.setAttribute("subject",subject); return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { } }
8、在interceptor
下新建TokenConfig
拦截器配置类,用于配置TokenInterceptor
,我们这里配置拦截所有请求,代码如下:
package com.panziye.jwtdemo.interceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class TokenConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { //拦截所有请求 registry.addInterceptor(tokenInterceptor()) .addPathPatterns("/**"); } @Bean public TokenInterceptor tokenInterceptor() { return new TokenInterceptor(); } }
1、在项目中新建model
包,在model
下新建User
实体类,代码如下:
package com.panziye.jwtdemo.model; public class User { private Long userId; private String username; private String password; public User(){} public User(Long userId, String username, String password) { this.userId = userId; this.username = username; this.password = password; } public Long getUserId() { return userId; } public void setUserId(Long userId) { this.userId = userId; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } }
2、在项目中新建controller
包,在controller
下新建UserController
实体类,代码如下:
package com.panziye.jwtdemo.controller; import com.panziye.jwtdemo.annotation.PassLogin; import com.panziye.jwtdemo.http.ResponseResult; import com.panziye.jwtdemo.model.User; import com.panziye.jwtdemo.util.JwtUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @RestController @RequestMapping("/user") public class UserController { private static final Logger logger = LoggerFactory.getLogger(JwtUtil.class); @Autowired private JwtUtil jwtUtil; @PostMapping("/login") @PassLogin public ResponseResult login(User user, HttpServletResponse response){ //在此省略了service和dao层代码 if("admin".equals(user.getUsername()) && "123456".equals(user.getPassword())){ logger.info("登录成功"); //生成Token,以用户id作为载荷中的subject去创建 String token = jwtUtil.createToken("1"); logger.info("token="+token); User u = new User(1L,"admin","123456"); response.setHeader(jwtUtil.getHeader(),"Bearer "+token); ResponseResult result = ResponseResult.ok(u); return result; }else { logger.info("登录失败"); return ResponseResult.invalid_request(); } } @GetMapping("/findUser") public ResponseResult findUser(HttpServletRequest request){ //在拦截器中已经将从token中解析好的subject存入了request域,所以这里能获取到 Long userId = Long.parseLong(request.getAttribute("subject").toString()); System.out.println("发起请求用户的id===="+userId); //模拟返回一个数据 ResponseResult result = ResponseResult.ok(userId); return result; } }
3、使用Postman工具测试,Postman工具下载地址:点击去下载
1)测试正常登录,发送登录post请求,携带用户名和密码参数,发现用户信息正常响应了
再查看下Header
响应头,发现Authorization
有返回的Token令牌(不包含Bearer
),我们复制下这个令牌,用于后面测试。
2)测试findUser
的get请求,我们先不携带Token直接,发送get请求,发现返回401无权限
然后我们在Header
中的Type
选择Bearer Token
,把我们刚才复制的Token粘贴进去,再发送该请求,发现正常响应数据: