本文介绍了Spring Security单点登录的概念和基本原理。单点登录是指用户只需登录一次,即可在多个相互信任的系统中实现无缝访问和授权。通过Spring Security框架的支持,可以实现有效的用户管理和权限控制。最后,本文提供了实际应用案例,展示了Spring Security单点登录的实际应用和效果。通过学习本文,读者可以了解并掌握Spring Security单点登录的基本概念和实现方法,从而提高系统的用户体验和安全性。
一、登录接口
1,登录流程
为什么需要先以用户名为key存储UUID,再以UUID为key存储JWT Token?
安全性:直接使用用户名为key存储JWT Token存在安全风险。因为用户名是用户公开的信息,如果Token存储方式与用户名直接关联,那么攻击者可能会尝试猜测或枚举用户名来获取Token。而UUID是一个随机生成的唯一标识符,不易被猜测,因此使用UUID作为key存储Token可以提高安全性。
解耦:将用户名和Token的存储解耦可以提高系统的灵活性和可扩展性。例如,如果未来需要更改用户名的存储方式或进行用户数据迁移,使用UUID作为Token的key可以确保Token的存储不受影响。
Token管理:使用UUID作为中间步骤还可以简化Token的管理。例如,如果需要重置Token或进行Token续期操作,系统可以方便地通过UUID找到对应的Token并进行处理。
2,接口地址
/**
* 登录接口
*/
@RestController
@Api(tags = "用户登录")
@RequestMapping("security")
public class LoginController {
@Autowired
LoginService loginService;
@PostMapping("login")
@ApiOperation(value = "用户登录",notes = "用户登录")
@ApiImplicitParam(name = "userVo",value = "登录对象",required = true,dataType = "UserVo")
@ApiOperationSupport(includeParameters ={"userVo.username","userVo.password"} )
public ResponseResult<UserVo> login(@RequestBody UserVo userVo){
return ResponseResult.success(loginService.login(userVo));
}
}
3,自定义认证用户
/**
* 自定认证用户
*/
@Data
@NoArgsConstructor
public class UserAuth implements UserDetails {
private String id;
/**
* 用户账号
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 权限内置
*/
private Collection<SimpleGrantedAuthority> authorities;
/**
* 用户类型(00系统用户)
*/
private String userType;
/**
* 用户昵称
*/
private String nickName;
/**
* 用户邮箱
*/
private String email;
/**
* 真实姓名
*/
private String realName;
/**
* 手机号码
*/
private String mobile;
/**
* 用户性别(0男 1女 2未知)
*/
private String sex;
/**
* 创建者
*/
private Long createBy;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新者
*/
private Long updateBy;
/**
* 更新时间
*/
private LocalDateTime updateTime;
/**
* 备注
*/
private String remark;
/**
* 部门编号【当前】
*/
private String deptNo;
/**
* 职位编号【当前】
*/
private String postNo;
public UserAuth(UserVo userVo) {
this.setId(userVo.getId().toString());
this.setUsername(userVo.getUsername());
this.setPassword(userVo.getPassword());
if (!EmptyUtil.isNullOrEmpty(userVo.getResourceRequestPaths())) {
authorities = new ArrayList<>();
userVo.getResourceRequestPaths().forEach(resourceRequestPath -> authorities.add(new SimpleGrantedAuthority(resourceRequestPath)));
}
this.setUserType(userVo.getUserType());
this.setNickName(userVo.getNickName());
this.setEmail(userVo.getEmail());
this.setRealName(userVo.getRealName());
this.setMobile(userVo.getMobile());
this.setSex(userVo.getSex());
this.setCreateTime(userVo.getCreateTime());
this.setCreateBy(userVo.getCreateBy());
this.setUpdateTime(userVo.getUpdateTime());
this.setUpdateBy(userVo.getUpdateBy());
this.setRemark(userVo.getRemark());
this.setDeptNo(userVo.getDeptNo());
this.setPostNo(userVo.getPostNo());
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
4,定义UserDetailService实现类
@Component
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserVo userVo = userService.findUserVoForLogin(username);
return new UserAuth(userVo);
}
}
5,登录接口逻辑实现
public UserVo login(LoginDto loginDto) {
// 1.获取认证用户
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginDto.getUsername(), loginDto.getPassword());
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
if (!authenticate.isAuthenticated()) {
throw new BaseException("登录失败");
}
UserAuth principal = (UserAuth) authenticate.getPrincipal();
UserVo user = BeanUtil.copyProperties(principal, UserVo.class);
// 2.获取当前角色列表
List<RoleVo> roleList = roleService.findRoleVoListByUserId(user.getId());
Set<String> roleLabels = roleList.stream().map(RoleVo::getLabel).collect(Collectors.toSet());
user.setRoleLabels(roleLabels);
// 3.获取用户资源表示
List<ResourceVo> resourceList = resourceService.findResourceVoListByUserId(user.getId());
Set<String> requestPaths = resourceList.stream()
.filter(resourceVo -> resourceVo.getResourceType().equals("r"))
.map(ResourceVo::getRequestPath)
.collect(Collectors.toSet());
user.setResourceRequestPaths(requestPaths);
user.setPassword("");
// 4.颁发Token
Map<String, Object> claims = new HashMap<>();
claims.put("currentUser", JSON.toJSONString(user));
String token = JwtUtil.createJWT(jwtTokenManagerProperties.getBase64EncodedSecretKey(),
jwtTokenManagerProperties.getTtl(), claims);
// 5.生成UUID存储Redis
String uuid = UUID.randomUUID().toString();
String userTokenKey = UserCacheConstant.USER_TOKEN+user.getUsername();
long ttl = jwtTokenManagerProperties.getTtl() / 1000;
redisTemplate.opsForValue().set(userTokenKey, uuid,
ttl, TimeUnit.SECONDS);
// 6.将Token存储Redis
String jwtTokenKey = UserCacheConstant.JWT_TOKEN + uuid;
redisTemplate.opsForValue().set(jwtTokenKey, token,
ttl, TimeUnit.SECONDS);
// 7.返回
user.setUserToken(uuid);
return user;
}
二、自定义授权管理器
1,定义管理器
@Component
@Slf4j
public class JwtAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private JwtTokenManagerProperties jwtTokenManagerProperties;
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication,
RequestAuthorizationContext requestContext) {
// 1.获取jwtToken
String userUUID = requestContext.getRequest().getHeader(SecurityConstant.USER_TOKEN);
if (EmptyUtil.isNullOrEmpty(userUUID)){
return new AuthorizationDecision(false);
}
String jwtTokenKey = UserCacheConstant.JWT_TOKEN+userUUID;
String jwtToken = redisTemplate.opsForValue().get(jwtTokenKey);
if (EmptyUtil.isNullOrEmpty(jwtToken)){
return new AuthorizationDecision(false);
}
// 2.解析jwtToken
String userJSON = JwtUtil.parseJWT(jwtTokenManagerProperties.getBase64EncodedSecretKey(),
jwtToken).get("currentUser").toString();
UserVo userVo = JSON.parseObject(userJSON, UserVo.class);
if (EmptyUtil.isNullOrEmpty(userVo.getUsername())) {
return new AuthorizationDecision(false);
}
// 3.判断uuid是否相同
String userTokenKey = UserCacheConstant.USER_TOKEN + userVo.getUsername();
String uuid = redisTemplate.opsForValue().get(userTokenKey);
if (Objects.equals(uuid, userUUID)) {
return new AuthorizationDecision(false);
}
// 4.重置过期时间
Long expire = redisTemplate.opsForValue().getOperations()
.getExpire(jwtTokenKey);
if (expire.longValue() <= 600) {
//jwt生成的token也会过期,故需要再次生成
Map<String, Object> claims = new HashMap<>();
claims.put("currentUser", JSON.toJSONString(userVo));
String token = JwtUtil.createJWT(jwtTokenManagerProperties.getBase64EncodedSecretKey(),
jwtTokenManagerProperties.getTtl(), claims);
long ttl = jwtTokenManagerProperties.getTtl() / 1000;
redisTemplate.opsForValue().set(jwtTokenKey, token, ttl, TimeUnit.SECONDS);
redisTemplate.expire(userTokenKey, ttl, TimeUnit.SECONDS);
}
return new AuthorizationDecision(true);
}
}
2,权限配置
/**
* 权限核心配置类
*/
@Configuration
@EnableConfigurationProperties(SecurityConfigProperties.class)
public class SecurityConfig {
@Autowired
SecurityConfigProperties securityConfigProperties;
@Autowired
JwtAuthorizationManager jwtAuthorizationManager;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
//忽略地址
List<String> ignoreUrl = securityConfigProperties.getIgnoreUrl();
http.authorizeHttpRequests()
.antMatchers( ignoreUrl.toArray( new String[ignoreUrl.size() ] ) )
.permitAll()
.anyRequest().access(jwtAuthorizationManager);
http.csrf().disable();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS );//关闭session
http.headers().cacheControl().disable();//关闭缓存
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
/**
* BCrypt密码编码
* @return
*/
@Bean
public BCryptPasswordEncoder bcryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
三、解析用户
1,定义拦截器
用户登录后获token,请求携token验证。用ThreadLocal存当前线程用户数据,实现线程隔离与数据共享
/**
* 多租户放到SubjectContent上下文中
*/
@Component
public class UserTokenIntercept implements HandlerInterceptor {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private JwtTokenManagerProperties jwtTokenManagerProperties;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String userUUID = request.getHeader(SecurityConstant.USER_TOKEN);
if (!StringUtils.hasText(userUUID)) {
throw new Exception("用户登录状态异常");
}
String jwtTokenKey = UserCacheConstant.JWT_TOKEN + userUUID;
String jwtToken = redisTemplate.opsForValue().get(jwtTokenKey);
if (!StringUtils.hasText(jwtToken)) {
throw new Exception("用户登录状态异常");
}
Claims claims = JwtUtil.parseJWT(jwtTokenManagerProperties.getBase64EncodedSecretKey(), jwtToken);
String user = claims.get("currentUser").toString();
UserThreadLocal.setSubject(user);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserThreadLocal.removeSubject();
}
}
2,配置生效
/**
* webMvc高级配置
// */
@Configuration
@ComponentScan("springfox.documentation.swagger.web")
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
UserTokenIntercept userTokenIntercept;
@Override
public void addInterceptors(InterceptorRegistry registry) {;
registry.addInterceptor(userTokenIntercept).excludePathPatterns("/**");
}
/**
* 资源路径 映射
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
//支持webjars
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
//支持swagger
registry.addResourceHandler("swagger-ui.html")
.addResourceLocations("classpath:/META-INF/resources/");
//支持小刀
registry.addResourceHandler("doc.html")
.addResourceLocations("classpath:/META-INF/resources/");
}
public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";
@Bean
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
return builder -> {
// 序列化
builder.serializerByType(Long.class, ToStringSerializer.instance);
builder.serializerByType(BigInteger.class, ToStringSerializer.instance);
builder.serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));
builder.serializerByType(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));
builder.serializerByType(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
// 反序列化
builder.deserializerByType(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));
builder.deserializerByType(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));
builder.deserializerByType(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
};
}
}
注意:执行流程会先经过授权管理器,当授权管理器放行通过才到拦截器生效