目录
JWT单点登录
1、表结构
CREATE TABLE `user` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键,用户ID',
`username` varchar(255) NOT NULL COMMENT '用户名',
`password` varchar(255) NOT NULL COMMENT '密码',
`gender` int(1) DEFAULT NULL COMMENT '性别:0男,1女',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;
INSERT INTO `user` VALUES (1, 'admin', '123456', 1, '2023-11-03 12:14:16', NULL);
INSERT INTO `user` VALUES (2, 'test', '123456', 0, '2023-11-07 12:14:41', NULL);
2、依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.1</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.39</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.6.5</version>
</dependency>
<!-- Token生成与解析-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
</dependencies>
3、实体
User:用户实体
@Data
public class User {
private Long id;
private String username;
private String password;
private Integer gender;
private Date createTime;
private Date updateTime;
}
UserToken:token中存储的用户信息。因为token可以被解码,所以不要有敏感信息。
@Data
public class UserToken implements Serializable {
private Long id;
private String username;
private Integer gender;
}
4、JwtTokenUtil
用于生成和解析token。
public class JwtTokenUtil {
//定义token返回头部
public static final String AUTH_HEADER_KEY = "Authorization";
//token前缀
public static final String TOKEN_PREFIX = "Bearer ";
//签名密钥
public static final String SECRET = "q3t6w9z$C&F)J@NcQfTjWnZr4u7x";
//有效期默认为 2hour
public static final Long EXPIRATION_TIME = 1000L*60*60*2;
/**
* 创建Token
* @param content
* @return
*/
public static String createToken(String content) {
return TOKEN_PREFIX + Jwts.builder()
.setSubject(content)
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.signWith(SignatureAlgorithm.HS256, SECRET)
.compact();
}
/**
* 校验token
* @param token
* @return
* @throws Exception
*/
public static String verifyToken(String token) throws Exception {
try {
return Jwts.parser()
.setSigningKey(SECRET)
.parseClaimsJws(token.replace(TOKEN_PREFIX, ""))
.getBody()
.getSubject();
} catch (ExpiredJwtException e) {
throw new Exception("token已失效,请重新登录", e);
} catch (SignatureException e) {
throw new Exception("token验证失败!", e);
}
}
}
5、WebContextUtil
ThreadLocal缓存已经登录的用户。
public class WebContextUtil {
//本地线程缓存token
private static ThreadLocal<String> local = new ThreadLocal<>();
/**
* 设置token
* @param content
*/
public static void setUserToken(String content) {
removeUserToken();
local.set(content);
}
/**
* 获取token信息
* @return
*/
public static UserToken getUserToken() {
if (local.get() != null) {
UserToken userToken = JSONUtil.toBean(local.get(), UserToken.class);
return userToken;
}
return null;
}
/**
* 删除token信息
*/
public static void removeUserToken() {
if (local.get() != null) {
local.remove();
}
}
}
6、AuthenticationInterceptor(token解析)
public class AuthenticationInterceptor implements HandlerInterceptor {
/**
* handler参数:可以是HandlerAdapter存在的任何类型。最常用的是利用HandlerMethod的RequestMappingHandlerAdapter。
* 它可以是一个常规类、一个servlet,甚至是一个函数(当使用函数方法时)。
* Spring Web Services还有一个实现以及Spring Integration。
* Spring本身将支持:
* HandlerMethod
* HandlerFunction
* Conroller
* Servlet
* HttpRequestHandler
* WsdlDefinition (Spring Web Services)
* XsdDefinition (Spring Web Services)
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//从http请求头中获取token
String token = request.getHeader(JwtTokenUtil.AUTH_HEADER_KEY);
//如果不是映射到方法,放行(可能是请求静态资源等)
if (!(handler instanceof HandlerMethod)) {
return true;
}
//如果是方法探测,放行
if (HttpMethod.OPTIONS.equals(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
return true;
}
//如果方法有JwtIgnore注解,放行
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
if (method.isAnnotationPresent(JwtIgnore.class)) {
JwtIgnore jwtIgnore = method.getAnnotation(JwtIgnore.class);
if (jwtIgnore.value()) {
return true;
}
}
if (StrUtil.isBlank(token)) {
throw new Exception("token为空,鉴权失败!");
}
//验证token
String userToken = JwtTokenUtil.verifyToken(token);
//放入本地缓存
WebContextUtil.setUserToken(userToken);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//方法结束后,移除缓存的token
WebContextUtil.removeUserToken();
}
}
7、JwtIgnore
自定义注解,用于排除指定方法的token检验。
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface JwtIgnore {
boolean value() default true;
}
8、WebMvc配置
@Configuration
public class GlobalWebMvcConfig implements WebMvcConfigurer {
/**
* 跨域配置
* @param registry
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
// 添加映射路径
registry.addMapping("/**")
// 放行哪些原始域
.allowedOriginPatterns("*")
// 是否发送Cookie信息
.allowCredentials(true)
// 放行哪些原始域(请求方式)
.allowedMethods("*")
// 放行哪些原始域(头部信息)
.allowedHeaders("*")
// 暴露哪些头部信息(因为跨域访问默认不能获取全部头部信息)
.exposedHeaders("Server","Content-Length", "Authorization", "Access-Token", "Access-Control-Allow-Origin","Access-Control-Allow-Credentials");
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
//token拦截器
registry.addInterceptor(new AuthenticationInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/static/**");
}
}
9、UesrService
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public String login(String username, String password) throws Exception {
if (StrUtil.isBlank(username) || StrUtil.isBlank(password)) {
throw new Exception("用户名或密码为空!");
}
User user = getUserByName(username);
if (!password.equals(user.getPassword())) {
throw new Exception("密码错误!");
}
//创建token
UserToken userToken = BeanUtil.copyProperties(user, UserToken.class);
String token = JwtTokenUtil.createToken(JSONUtil.toJsonStr(userToken));
return token;
}
public User getUserByName(String username) {
return userMapper.getUserByName(username);
}
}
10、UserController
@RestController
public class UserController {
@Autowired
private UserService userService;
@JwtIgnore
@PostMapping("/login")
public Map<String, Object> login(@RequestParam("username")String username, @RequestParam("password")String password) throws Exception {
Map<String, Object> result = new HashMap<>();
String token = userService.login(username, password);
result.put("token", token);
return result;
}
@GetMapping("/test")
public Map<String, Object> test() {
Map<String, Object> result = new HashMap<>();
result.put("msg", "success");
result.put("data", WebContextUtil.getUserToken());
return result;
}
}
11、ExceptionController(全局异常处理)
@ControllerAdvice
public class ExceptionController {
@ResponseBody
@ExceptionHandler(Exception.class)
public Map<String, Object> handleException(Exception e) {
Map<String, Object> map = new HashMap<>();
map.put("code", 401);
map.put("message", e.getMessage());
return map;
}
}
12、测试
未登录直接访问:
登录:
登录后访问接口:
访问静态资源: