Springboot +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> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-boot-starter</artifactId> <version>3.0.0</version> </dependency> <dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <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 */@Slf4jpublic 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 */@Configurationpublic 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地址