概要
Spring Security 是一个认证授权的框架,记录配置整合过程和遇到的一些问题。(一种常规的配置方案,不展开描述 jwt 和 Redis, )
spring boot 2.7.7
整合步骤
首先来看一下默认状态下 Security 框架的Filter顺序
依赖库
<!-- security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- jwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
增加 UserDetails 实现类 SecurityUserDetailsBo
提供给 Security 的 UserDetails
@Data
@NoArgsConstructor
public class SecurityUserDetailsBo implements UserDetails {
// 接收数据返回的用户 PO ,login 过程中在 loadUserByUsername 中设置
private UmsUserPo umsUserPo;
// 接收数据返回的权限 PoList ,login 过程中在 loadUserByUsername 中设置
private List<UmsPermissionPo> umsPermissionPoList;
// 提供给 Security 鉴权, @JsonIgnore:在redis序列化中忽略
@JsonIgnore
private List<SimpleGrantedAuthority> authorities;
public SecurityUserDetailsBo(UmsUserPo umsUserPo, List<UmsPermissionPo> umsPermissionPoList) {
this.umsUserPo = umsUserPo;
this.umsPermissionPoList = umsPermissionPoList;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// jwt 鉴权过中,如果已经存在则会直接反,这里是一个优化,一个线程中你只登录的第一次需要
if (Objects.nonNull(authorities)){
return authorities;
}
// umsPermissionPoList => authorities
return umsPermissionPoList.stream()
.map(role ->new SimpleGrantedAuthority(role.getPermission()))
.collect(Collectors.toList());
}
// 返回登录用户账户名和密码 用于Security判断
@Override
public String getPassword() {
return umsUserPo.getPassword();
}
@Override
public String getUsername() {
return umsUserPo.getUserName();
}
// 提供了一些配置方案
// 账户没有过期
@Override
public boolean isAccountNonExpired() {
return true;
}
// 账户没有被锁定
@Override
public boolean isAccountNonLocked() {
return true;
}
// 身份认证是否是有效的
@Override
public boolean isCredentialsNonExpired() {
return true;
}
// 账户是否启用
@Override
public boolean isEnabled() {
return true;
}
}
增加 UserDetailsService 实现类 UmsAdminServiceImpl
@Service
public class UmsAdminServiceImpl extends ServiceImpl<UmsAdminMapper, UmsUserPo> implements UmsAdminService, UserDetailsService {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UmsPermissionMapper umsPermissionMapper;
@Autowired
private JwtUtil jwtUtil;
@Autowired
private RedisUtil redisUtil;
@Value("${redis.database}")
private String REDIS_DATABASE;
@Value("${redis.expire}")
private Long REDIS_EXPIRE;
@Override
public String login(UmsAdminLoginDto umsAdminLoginDto) {
// 使用 Security 中的UsernamePasswordAuthenticationToken -> authenticationToken
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(umsAdminLoginDto.getUserName(),umsAdminLoginDto.getPassword());
// 在SecurityConfig 中配置 AuthenticationManager 会调用 loadUserByUsername
Authentication authentication = authenticationManager.authenticate(authenticationToken);
SecurityUserDetailsBo securityUserDetailsBo = (SecurityUserDetailsBo) authentication.getPrincipal();
String userFlag = securityUserDetailsBo.getUsername().toString();
// 生成token
String userToken = jwtUtil.generateToken(userFlag);
String key = REDIS_DATABASE + ":" + securityUserDetailsBo.getUsername();
// token 存储入 redis
redisUtil.set(key,securityUserDetailsBo,REDIS_EXPIRE);
return userToken;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
LambdaQueryWrapper<UmsUserPo> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(UmsUserPo::getUserName,username);
UmsUserPo umsUserPo = this.getOne(queryWrapper);
// 如果用户不存在
if(Objects.isNull(umsUserPo)){
throw new UsernameNotFoundException("用户不存在");
}
// 查询权限
List<UmsPermissionPo> permissionList = umsPermissionMapper.getPermissionList(umsUserPo.getUserId());
return new SecurityUserDetailsBo(umsUserPo,permissionList);
}
}
添加过滤器 JwtAuthenticationTokenFilter
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private RestAuthenticationEntryPoint restAuthenticationEntryPoint;
@Autowired
private RedisUtil redisUtil;
@Autowired
private JwtUtil jwtUtil;
@Value("${jwt.toKenHeader}")
private String tokenHeader;
@Value("${jwt.tokenHead}")
private String tokenHead;
@Value("${redis.database}")
private String REDIS_DATABASE;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
String authHeader = request.getHeader(this.tokenHeader);
if(StringUtils.hasText(authHeader) && authHeader.startsWith(this.tokenHead)){
String authToken = authHeader.substring(this.tokenHead.length()).trim();
// 获取jwt中的username
String username = jwtUtil.getUserNameFromToken(authToken);
if ( !Objects.isNull(username) && SecurityContextHolder.getContext().getAuthentication() == null){
// 检验 jwt 是否生效
if(jwtUtil.validateToken(authToken,username)){
// 通过 jwt 中的username 拼接redis key 获取用户信息
String key = REDIS_DATABASE + ":" + username;
SecurityUserDetailsBo securityUserDetailsBo = (SecurityUserDetailsBo) redisUtil.get(key);
// 从 redis 中获取数据
if(!Objects.isNull(securityUserDetailsBo) ){
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(securityUserDetailsBo,null,securityUserDetailsBo.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}else {
throw new ExpiredJwtException("redis 未查询到");
}
} else {
throw new ExpiredJwtException("token失效或异常");
}
}else {
throw new ExpiredJwtException("header中的token异常");
}
}
filterChain.doFilter(request,response);
}catch (AuthenticationException ex){
// 目前得知 doFilterInternal 中抛出异常的最佳方式, AuthenticationEntryPoint 可以获得 ex.message,
// message 可以设置成 ResultCode,从而在AuthenticationEntryPoint中枚举 异常message
SecurityContextHolder.clearContext();
this.restAuthenticationEntryPoint.commence(request, response, ex);
}
}
}
配置 SecurityConfig
@Configuration
// 开启权限
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
@Autowired
private RestAuthenticationEntryPoint restAuthenticationEntryPoint;
@Autowired
private RestfulAccessDeniedHandler restfulAccessDeniedHandler;
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Bean
AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 关闭 csrf
.csrf().disable()
// 关闭 session
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// 添加认证和授权异常回调
.exceptionHandling()
.accessDeniedHandler(restfulAccessDeniedHandler)
.authenticationEntryPoint(restAuthenticationEntryPoint)
.and()
.authorizeRequests()
.antMatchers("/admin/login").anonymous()
// swagger knife4j资源放行
.antMatchers("/favicon.ico").anonymous()
.antMatchers("/swagger-ui/**").anonymous()
.antMatchers("/swagger-resources/**").anonymous()
.antMatchers("/profile/**").anonymous()
.antMatchers("/profile/**").anonymous()
.antMatchers("/v3/**").anonymous()
.antMatchers("/doc.html").anonymous()
.antMatchers("/webjars/**").anonymous()
.anyRequest().authenticated()
.and()
// 添加jwt 顾虑器
.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
配置授权
@ApiOperation(value = "管理菜单")
@RequestMapping(value = "/menu01", method = RequestMethod.GET)
@PreAuthorize("hasAuthority('admin:menu:one')")
@ResponseBody
public CommonResult menu01() {
return CommonResult.success("管理菜单");
}
@ApiOperation(value = "员工菜单")
@RequestMapping(value = "/menu02", method = RequestMethod.GET)
@PreAuthorize("hasAuthority('employee:menu:one')")
@ResponseBody
public CommonResult menu02() {
return CommonResult.success("员工菜单");
}
通过 PreAuthorize 配置授权即可,hasAuthority 实际执行逻辑,如果想封装自己的权限信息
如下:
// 注入到容器
@Component("sp")
public class SecurityPermission {
public boolean hasAuthority(){
// 需要的鉴权逻辑
// ....
return true;
}
}
// 使用
@PreAuthorize("@sp.hasAuthority('employee:menu:one')")
public CommonResult menu02() {
return CommonResult.success("员工菜单");
}
其他方案
# 别再用过时的方式了!全新版本Spring Security,这样用才够优雅!
# 仅需四步,整合SpringSecurity+JWT实现登录认证 !