用Spring Boot和JWT完成全流程的登录验证、密码加密及Token检查教程(包含源代码示例)
JWT实现登录认证
- 简介
- 环境
- 1. 依赖
- 2. token生成及校验
- 3. 登录
- 4. 编写拦截器进行token校验
- 5. 源码下载
简介
-
通俗地说,JWT的本质就是一个字符串,它是将用户信息保存到一个Json字符串中,然后进行编码后得到一个JWT token,并且这个JWT token带有签名信息,接收后可以校验是否被篡改,所以可以用于在各方之间安全地将信息作为Json对象传输。JWT的认证流程如下:
-
首先,前端通过Web表单将自己的用户名和密码发送到后端的接口,这个过程一般是一个POST请求。建议的方式是通过SSL加密的传输(HTTPS),从而避免敏感信息被嗅探
-
后端核对用户名和密码成功后,将包含用户信息的数据作为JWT的Payload,将其与JWT Header分别进行Base64编码拼接后签名,形成一个JWT Token,形成的JWT Token就是一个如同lll.zzz.xxx的字符串
-
后端将JWT Token字符串作为登录成功的结果返回给前端。前端可以将返回的结果保存在浏览器中,退出登录时删除保存的JWT Token即可
-
前端在每次请求时将JWT Token放入HTTP请求头中的Authorization属性中(解决XSS和XSRF问题)
-
后端检查前端传过来的JWT Token,验证其有效性,比如检查签名是否正确、是否过期、token的接收方是否是自己等等
-
验证通过后,后端解析出JWT Token中包含的用户信息,进行其他逻辑操作(一般是根据用户信息得到权限等),返回结果
本博客项目源码地址:
- 项目源码github地址
- 项目源码国内gitee地址
环境
本教程使用jdk11,其他环境自行测试
api测试工具
postman
,{{localhost}}
请自行更改为自己的地址,如localhost:9999
1. 依赖
- 1.1 pom导入依赖
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--swagger3 生成接口注释-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
<!-- 解决jdk11缺失jar包引起的报错-->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
</dependency>
<!-- jwt token核心依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- jwt token核心依赖-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.18.3</version>
</dependency>
2. token生成及校验
- 2.1 封装用户
存储用户的基本信息
import lombok.Data;
import lombok.experimental.Accessors;
/**
* @author l
*/
@Data
@Accessors(chain = true)
public class JwtUser {
private boolean valid;
private String userId;
private String role;
public JwtUser() {
this.valid = false;
}
}
- 2.2 编写JWT提供者
主要关注 createToken 和 checkToken 两个方法
- createToken 生成token
- checkToken 校验token
createToken
import io.jsonwebtoken.*;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.extern.slf4j.Slf4j;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Date;
/**
* date: 2021-01-05 08:48
* description token管理
*
* @author qiDing
*/
@Slf4j
@ApiModel("token提供者")
public class TokenProvider {
@ApiModelProperty("盐")
private static final String SALT_KEY = "links";
@ApiModelProperty("令牌有效期毫秒")
private static final long TOKEN_VALIDITY = 86400000;
@ApiModelProperty("权限密钥")
private static final String AUTHORITIES_KEY = "auth";
@ApiModelProperty("Base64 密钥")
private final static String SECRET_KEY = Base64.getEncoder().encodeToString(SALT_KEY.getBytes(StandardCharsets.UTF_8));
/**
* 生成token
* @param userId 用户id
* @param clientId 用于区别客户端,如移动端,网页端,此处可根据自己业务自定义
* @param role 角色权限
*/
public static String createToken(String userId, String clientId, String role) {
Date validity = new Date((new Date()).getTime() + TOKEN_VALIDITY);
return Jwts.builder()
// 代表这个JWT的主体,即它的所有人
.setSubject(String.valueOf(userId))
// 代表这个JWT的签发主体
.setIssuer("")
// 是一个时间戳,代表这个JWT的签发时间;
.setIssuedAt(new Date())
// 代表这个JWT的接收对象
.setAudience(clientId)
.claim("role", role)
.claim("userId", userId)
.signWith(SignatureAlgorithm.HS512, SECRET_KEY)
.setExpiration(validity)
.compact();
}
/**
* 校验token
*/
public static JwtUser checkToken(String token) {
if (validateToken(token)) {
Claims claims = Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();
String audience = claims.getAudience();
String userId = claims.get("userId", String.class);
String role = claims.get("role", String.class);
JwtUser jwtUser = new JwtUser().setUserId(userId).setRole(role).setValid(true);
log.info("===token有效{},客户端{}", jwtUser, audience);
return jwtUser;
}
log.error("***token无效***");
return new JwtUser();
}
private static boolean validateToken(String authToken) {
try {
Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(authToken);
return true;
} catch (Exception e) {
log.error("无效的token:" + authToken);
}
return false;
}
}
3. 登录
- 3.1 密码加密
对密码进行md5加密
import org.springframework.util.DigestUtils;
/**
* 密码加密工具类
*
* @author liangQiDing
*/
public class PasswordEncoder {
/**
* 密码加密
* @param rawPassword 登录时传入的密码
*/
public static String encode(CharSequence rawPassword) {
return DigestUtils.md5DigestAsHex(rawPassword.toString().getBytes());
}
/**
* 密码对比
* @param rawPassword 登录时传入的密码
* @param encodedPassword 数据库保存的加密过的密码
*/
public static boolean matches(CharSequence rawPassword, String encodedPassword) {
return encodedPassword.equals(DigestUtils.md5DigestAsHex(rawPassword.toString().getBytes()));
}
}
- 3.2 登录接口编写
import com.example.jwt_dome.config.PasswordEncoder;
import com.example.jwt_dome.jwt.AuthStorage;
import com.example.jwt_dome.jwt.JwtUser;
import com.example.jwt_dome.jwt.TokenProvider;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
/**
* @author liangQiDing
*/
@RestController
@Api("token测试服务器")
public class TokenController {
/**
* 模拟数据库数据 账号 admin 密码 123456
*/
private final static HashMap<String, String> USER = new HashMap<>() {
{
put("admin", "e10adc3949ba59abbe56e057f20f883e");
}
};
@GetMapping("/login")
@ApiOperation("登陆示例(账号admin,密码123456)")
public String login(String username, String password) {
if (PasswordEncoder.matches(password, USER.get(username))) {
// 模拟一个用户的数据 用户id为1 登录端为网页web 角色是admin
return TokenProvider.createToken("1", "web", "admin");
}
return "error";
}
@GetMapping("/token/validate")
@ApiOperation("token校验")
public JwtUser tokenValidate(String token) {
return TokenProvider.checkToken(token);
}
}
-
3.3 token获取测试
-
3.4 token校验测试
4. 编写拦截器进行token校验
- 4.1 存储授权信息
用于在我们授权通过后,在请求中获取用户的信息
/**
* 存储本次请求的授权信息,适用于各种业务场景,包括分布式部署
*
* @author lqd
*/
public class AuthStorage {
@ApiModelProperty("请求头token的下标")
public static final String TOKEN_KEY = "token";
/**
* 模拟session
*/
private static final HashMap<String, JwtUser> JWT_USER = new HashMap<String, JwtUser>();
/**
* 全局获取用户
*/
public static JwtUser getUser() {
HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
return JWT_USER.get(request.getHeader(TOKEN_KEY));
}
/**
* 设置用户
*/
public static void setUser(String token, JwtUser user) {
JWT_USER.put(token, user);
}
/**
* 清除授权
*/
public static void clearUser() {
HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
JWT_USER.remove(request.getHeader(TOKEN_KEY));
}
}
- 4.2 配置拦截器
在请求响应前,校验token,校验通过后存储用户信息。
import com.example.jwt_dome.jwt.AuthStorage;
import com.example.jwt_dome.jwt.JwtUser;
import com.example.jwt_dome.jwt.TokenProvider;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 拦截器
*
* @author lqd
*/
@Slf4j
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader(AuthStorage.TOKEN_KEY);
if (StringUtils.hasLength(token)) {
JwtUser jwtUser = TokenProvider.checkToken(token);
// 是否认证通过
if (jwtUser.isValid()) {
// 保存授权信息
AuthStorage.setUser(token, jwtUser);
return true;
}
}
response.setContentType("text/html;charset=utf-8");
response.getWriter().write("请先登录!");
return false;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 请求完成清除授权信息
AuthStorage.clearUser();
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}
- 4.3 配置拦截路径
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 配置拦截器路径
*
* @author lqd
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new AuthInterceptor())
// 拦截的路径
.addPathPatterns("/**")
// 开放的路径
.excludePathPatterns("/login/**", "/token/validate");
}
}
- 4.4 拦截测试
在controller层添加测试接口
@GetMapping("/get/Info")
@ApiOperation("模拟拦截")
public String getInfo() {
// 从全局环境中获取用户id
JwtUser user = AuthStorage.getUser();
return "用户:"+user.getUserId() + ",请求成功";
}
普通访问
请求头添加token后再访问
5. 源码下载
- Springboot开发脚手架,集合各种常用框架使用案例,完善的文档,致力于让开发者快速搭建基础环境并让应用跑起来。
- 项目源码国内gitee地址
- 项目源码github地址
上一篇: 如何使用JWT实现身份验证与授权