Spring Security 整合 jwt 源码地址
新建springBoot工程导入如下依赖
包括了后续需要用到的数据库操作相关的组件
<!--Spring security-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--SpringBoot druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<!-- Mysql Connector -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--SpringBoot druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<!--SpringBoot mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<!--Spring Web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
增加一个接口
@RestController
@RequestMapping("/user")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping("/{id}")
public R<Object> queryById(@PathVariable Long id){
new FilterChainProxy();
return R.ok( userService.queryById(id));
}
}
增加配置文件和数据库表结构
spring:
datasource:
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/zcct-user?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: zclvct
# 初始连接数
initial-size: 5
# 最小连接数
min-idle: 10
# 最大连接数
max-active: 20
# 获取连接超时时间
max-wait: 5000
# 连接有效性检测时间
time-between-eviction-runs-millis: 60000
# 连接在池中最小生存的时间
min-evictable-idle-time-millis: 300000
# 连接在池中最大生存的时间
max-evictable-idle-time-millis: 900000
test-while-idle: true
test-on-borrow: false
test-on-return: false
# 检测连接是否有效
validation-query: select 1
# 配置监控统计
webStatFilter:
enabled: true
stat-view-servlet:
enabled: true
url-pattern: /druid/*
reset-enable: false
filter:
stat:
enabled: true
# 记录慢SQL
log-slow-sql: true
slow-sql-millis: 1000
merge-sql: true
wall:
config:
multi-statement-allow: true
security:
user:
name: admin
password: admin
server:
port: 9001
CREATE TABLE `sys_user` (
`user_id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`dept_id` bigint DEFAULT NULL COMMENT '部门ID',
`tenant_id` bigint DEFAULT NULL COMMENT '租户ID',
`user_name` varchar(30) NOT NULL COMMENT '用户账号',
`nick_name` varchar(30) NOT NULL COMMENT '用户昵称',
`user_type` tinyint DEFAULT '0' COMMENT '用户类型(0管理员用户, 01系统用户 )',
`email` varchar(50) DEFAULT '' COMMENT '用户邮箱',
`phone` varchar(11) DEFAULT '' COMMENT '手机号码',
`sex` tinyint DEFAULT '0' COMMENT '用户性别(0男 1女 2未知)',
`avatar` varchar(100) DEFAULT '' COMMENT '头像地址',
`password` varchar(100) DEFAULT '' COMMENT '密码',
`status` tinyint DEFAULT '0' COMMENT '帐号状态(0正常 1停用)',
`login_ip` varchar(128) DEFAULT '' COMMENT '最后登录IP',
`login_date` datetime DEFAULT NULL COMMENT '最后登录时间',
`create_by` varchar(64) DEFAULT '' COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(64) DEFAULT '' COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`is_deleted` tinyint DEFAULT '0' COMMENT '删除标志',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=100 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户信息表';
启动springBoot功能 访问 /user/1 接口
上面提到的快速入门操作就是 Spring Security 提供的 HttpBasic登录验证模式 ,他是最简单的登录认证模式
他的执行流程基本如下图所示
因为 spring boot 集成 Spring Security,通过 spring boot 的自动装配,其实在启动项目时做了很多操作
JWT是 Json Web Token 的缩写。它是基于 RFC 7519 标准定义的一种可以安全传输的 小巧 和 自包含 的JSON对象。由于数据是使用数字签名的,所以是可信任的和安全的。JWT可以使用HMAC算法对secret进行加密或者使用RSA的公钥私钥对来进行签名。
利用 Spring Security 过滤器链,根据前端传递的的token信息
整理执行流程如下流程如下:
### 增加 WebSecurityConfig 配置类
```java
private final ApplicationContext applicationContext;
private final JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Bean
public PasswordEncoder passwordEncoder() {
// 密码加密方式
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 关闭 csrf
.csrf().disable()
.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
// 不通过session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// 防止iframe 造成跨域
.and()
.headers()
.frameOptions()
.disable()
.and()
.authorizeRequests()
// login 允许匿名访问
.antMatchers("/auth/login").permitAll();
}
在 jwtAuthenticationTokenFilter 实现对请求的token进行解析和验证 ,jwtAuthenticationTokenFilter 应放在 UsernamePasswordAuthenticationFilter 过滤器之前,在验证账户名和密码前设置 UsernamePasswordAuthenticationToken
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
private final JwtTokenProvider jwtTokenProvider;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = request.getHeader("Authorization");
if(StrUtil.isNotBlank(token)) {
try{
jwtTokenProvider.parseJwt(token);
}catch (Exception e) {
e.printStackTrace();
log.debug("非法Token:{}", token);
}
}
filterChain.doFilter(request,response);
}
}
@Slf4j
@Component
public class JwtTokenProvider {
@Resource
private RedisUtil redisUtil;
public static final String AUTHORITIES_KEY = "user";
private JwtParser jwtParser;
private JwtBuilder jwtBuilder;
private static String BASE64_SECRET = "ZmQ0ZGI5NjQ0MDQwY2I4MjMxY2Y3ZmI3MjdhN2ZmMjNhODViOTg1ZGE0NTBjMGM4NDA5NzYxMjdjOWMwYWRmZTBlZjlhNGY3ZTg4Y2U3YTE1ODVkZDU5Y2Y3OGYwZWE1NzUzNWQ2YjFjZDc0NGMxZWU2MmQ3MjY1NzJmNTE0MzI=";
/**
* 初始化
*/
@PostConstruct
public void init() {
Key key = generalKey();
jwtParser = Jwts.parserBuilder()
.setSigningKey(key)
.build();
jwtBuilder = Jwts.builder()
.signWith(key, SignatureAlgorithm.HS512);
}
/**
* 由字符串生成加密key
* @return
*/
public static Key generalKey() {
byte[] encodedKey = Base64.decodeBase64(BASE64_SECRET);
Key key = Keys.hmacShaKeyFor(encodedKey);
return key;
}
/**
* 创建Token
* @param userId
* @return
*/
public String createToken(String userId, JwtUser jwtUser) {
String token = jwtBuilder
// 加入ID确保生成的 Token 都不一致
.setId(IdUtil.simpleUUID())
.claim(AUTHORITIES_KEY, userId)
.setSubject(userId)
.compact();
redisUtil.set("login:"+userId,jwtUser,1L, TimeUnit.HOURS);
return token;
}
/**
* 解析 jtw 保存
* @param token
*/
public void parseJwt(String token) {
if (StringUtils.hasText(token) && token.startsWith("Bearer ")) {
// 去掉令牌前缀
token = token.replace("Bearer ", "");
} else {
log.debug("非法Token:{}", token);
}
Claims claims = jwtParser.parseClaimsJws(token).getBody();
String userId = claims.getSubject();
JwtUser jwtUser = (JwtUser)redisUtil.get("login:" + userId);
if(ObjectUtil.isEmpty(jwtUser)) {
throw new RuntimeException("请求未认证");
}
Object authoritiesStr = claims.get(AUTHORITIES_KEY);
Collection<? extends GrantedAuthority> authorities =
ObjectUtil.isNotEmpty(authoritiesStr) ?
Arrays.stream(authoritiesStr.toString().split(","))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList()) : Collections.emptyList();
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(jwtUser,"",authorities);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
public Map<String, Object> login(LoginUser loginUser) {
String username = loginUser.getUsername();
String password = loginUser.getPassword();
// 生成一个 AuthenticationToken
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username,password);
// 使用 authenticationManager 进行认证
Authentication authentication = authenticationManagerBuilder.getObject().authenticate(token);
JwtUser jwtUser = (JwtUser)authentication.getPrincipal();
Long userId = jwtUser.getUser().getUserId();
String jwtToken = jwtTokenProvider.createToken(String.valueOf(userId),jwtUser);
Map<String, Object> authInfo = new HashMap<String, Object>(2) {{
put("token", "Brear " + jwtToken);
put("user", userId);
}};
return authInfo;
}
在调用 authenticationManagerBuilder.getObject().authenticate(token); 时, 会使用UserDetailsService加载用户信息进行验证,在这里需要增加自己的查询用户信息逻辑,并生成返回前端的token
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userService.findByUserName(username);
if (user == null) {
throw new BadRequestException("账号不存在");
}
if(user.getStatus() == 1) {
throw new BadRequestException("账号已停用");
}
List<GrantedAuthority> authorities = roleService.getGrantedAuthorities(user);
Set<String> roleKeys = roleService.getRoleKeys(user);
return new JwtUser(user,roleKeys,authorities);
}
@Aspect
@Component
public class AuthorizeAspect {
/**
* 配置切入点
*/
@Pointcut("@annotation(com.zcct.security.demo.security.annotation.RequireRules) " +
"|| @annotation(com.zcct.security.demo.security.annotation.RequiresLogin) " +
"|| @annotation(com.zcct.security.demo.security.annotation.RequiresPermissions) " +
"|| @annotation(com.zcct.security.demo.security.annotation.RequireAnonymous)")
public void pointcut() {
// 该方法无方法体,主要为了让同类中其他方法使用此切入点
}
@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
authorize(method);
Object obj = joinPoint.proceed();
return obj;
}
private void authorize(Method method) {
RequireRules requireRules = method.getAnnotation(RequireRules.class);
if(ObjectUtil.isNotEmpty(requireRules)) {
SecurityUtils.checkRules(requireRules.value());
}
RequiresLogin requiresLogin = method.getAnnotation(RequiresLogin.class);
if(ObjectUtil.isNotEmpty(requiresLogin)) {
SecurityUtils.checkLogin();
}
RequiresPermissions requiresPermissions = method.getAnnotation(RequiresPermissions.class);
if(ObjectUtil.isNotEmpty(requiresPermissions)) {
SecurityUtils.checkPermissions(requiresPermissions.value());
}
RequireAnonymous requireAnonymous = method.getAnnotation(RequireAnonymous.class);
if(ObjectUtil.isNotEmpty(requireAnonymous)) {
SecurityUtils.checkAnonymous();
}
}
}
增加工具类
public class SecurityUtils {
/**
* 获取当前登录用户
* 建议 保存在
* @return
*/
public static JwtUser getCurrentUser() {
UserDetailsService userDetailsService = SpringUtil.getBean(UserDetailsService.class);
return (JwtUser)userDetailsService.loadUserByUsername(getCurrentUsername());
}
/**
* 获取当前登录用户名
* @return
*/
public static String getCurrentUsername() {
final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
throw new BadRequestException(HttpStatus.FORBIDDEN,"当前登录状态过期");
}
if (authentication.getPrincipal() instanceof UserDetails) {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
return userDetails.getUsername();
}
throw new BadRequestException(HttpStatus.FORBIDDEN,"找不到当前登录的信息");
}
/**
* 检查角色权限
* @param rules
*/
public static void checkRules(String[] rules) {
Set<String> ruleKeys = SecurityUtils.getCurrentUser().getRuleKeys();
boolean hasAuthority = ruleKeys.contains("admin") || Arrays.stream(rules).anyMatch(ruleKeys::contains);
if(!hasAuthority) {
throw new BadRequestException(HttpStatus.UNAUTHORIZED,"没有权限");
}
}
/**
* 检查是否登录
*/
public static void checkLogin() {
getCurrentUsername();
}
/**
* 检查权限
* @param permissions
*/
public static void checkPermissions(String[] permissions) {
// 获取所有权限
List<String> allPermissions = SecurityUtils.getCurrentUser().getAuthorities().
stream().map(GrantedAuthority::getAuthority).
collect(Collectors.toList());
boolean hasAuthority = allPermissions.contains("admin") || Arrays.stream(permissions).anyMatch(allPermissions::contains);
if(!hasAuthority) {
throw new BadRequestException(HttpStatus.UNAUTHORIZED,"没有权限");
}
}
public static void checkAnonymous() {
UserDetails userDetails = null;
try{
userDetails = getCurrentUser();
}catch (Exception e){
}
if(ObjectUtil.isNotEmpty(userDetails)) {
throw new BadRequestException(HttpStatus.UNAUTHORIZED,"不允许访问");
}
}
}