SpringBoot整合SpringSecurity(进阶篇)
- 一、数据库
- 二、引入依赖
- 三、SpringSecurity 配置
- 四、各种处理器
-
- 1.AuthenticationEntryPointImpl
- 2.AccessDeniedHandlerImpl
- 3.AuthenticationSuccessHandlerImpl
- 4.AuthenticationFailHandlerImpl
- 5.LogoutSuccessHandlerImpl
- 五、加载数据库中的权限信息
- 六、访问决策管理器
- 七、从数据库中查找用户信息
- 八、菜单信息
- 补充
“风丶宇个人博客”这个项目使用了SpringSecurity去管理认证授权。SpringSecurity的内容是非常多的,要想完全驾驭只看本篇是不够用的,建议去看B站编程不良人的SpringSecurity,内容介绍较为全面,我愿称为B站最强的SpringSecurity教程。
下面开始介绍如何在项目中使用:
一、数据库
俗话说完事开头难,所以要给数据库建好。这里我就不给建表语句了,我给出表的关系图,大家可以更具自己的情况来建表。
二、引入依赖
引入springsecurity依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
三、SpringSecurity 配置
首先创建WebSecurityConfig继承了WebSecurityConfigurerAdapter,里面包含了Security的大部分配置。
/**
* chenjiayan
* 2022/12/23
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
// 未认证认证处理器
@Autowired
private AuthenticationEntryPointImpl authenticationEntryPoint;
// 权限不足处理器
@Autowired
private AccessDeniedHandlerImpl accessDeniedHandler;
// 认证成功处理器
@Autowired
private AuthenticationSuccessHandlerImpl authenticationSuccessHandler;
// 认证失败处理器
@Autowired
private AuthenticationFailHandlerImpl authenticationFailHandler;
// 退出登录处理器
@Autowired
private LogoutSuccessHandlerImpl logoutSuccessHandler;
@Bean
public FilterInvocationSecurityMetadataSource securityMetadataSource(){
// 接口拦截规则
return new FilterInvocationSecurityMetadataSourceImpl();
}
@Bean
public AccessDecisionManager accessDecisionManager(){
// 访问决策管理器
return new AccessDecisionManagerImpl();
}
@Bean
public SessionRegistry sessionRegistry(){
// 会话注册(方法)使用本地缓存保存 TODO 可替换为redis实现
return new SessionRegistryImpl();
}
/**
* 监听会话的创建和过期,过期移除
* @return HttpSessionEventPublisher
*/
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher(){
return new HttpSessionEventPublisher();
}
/**
* 密码加密
* @return {@link PasswordEncoder} 加密方式
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 认证
http.formLogin()
.loginProcessingUrl("/login")
.successHandler(authenticationSuccessHandler)
.failureHandler(authenticationFailHandler)
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessHandler(logoutSuccessHandler);
// 授权
http.authorizeRequests()
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O o) {
// 自定义权限设置
o.setSecurityMetadataSource(securityMetadataSource());
// 自定义权限校验
o.setAccessDecisionManager(accessDecisionManager());
return o;
}
})
.anyRequest().permitAll()
.and()
// 关闭跨站请求防护
.csrf().disable()
// 异常处理
.exceptionHandling()
// 未登录处理
.authenticationEntryPoint(authenticationEntryPoint)
// 权限不足处理
.accessDeniedHandler(accessDeniedHandler)
.and()
.sessionManagement() // 开启会话管理
.maximumSessions(20) // 设置最大会话数
.sessionRegistry(sessionRegistry()); // 自定义会话存储
}
}
四、各种处理器
1.AuthenticationEntryPointImpl
未认证处理
/**
* 用户未登录处理
* @author chenjiayan
* @date 2022/12/23
*/
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException {
response.setContentType(APPLICATION_JSON);
response.getWriter().write(JSON.toJSONString(Result.fail(StatusCodeEnum.NO_LOGIN)));
}
}
2.AccessDeniedHandlerImpl
权限不足处理器
/**
* 用户权限不通过处理
*
* @author chenjiayan
* @date 2022/12/23
*/
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
response.setContentType(APPLICATION_JSON);
response.getWriter().write(JSON.toJSONString(Result.fail("权限不足")));
}
}
3.AuthenticationSuccessHandlerImpl
认证成功处理器
/**
* 登录成功处理器
* @author chenjiayan
* @date 2022/12/25
*/
@Component
@Slf4j
@EnableAsync(proxyTargetClass=true) // 开启异步任务
public class AuthenticationSuccessHandlerImpl implements AuthenticationSuccessHandler {
@Autowired
private UserAuthServiceImpl userAuthService;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
// 返回登录信息 //TODO 优化 直接从authentication中获取用户信息
UserInfoDTO userInfoDTO = BeanCopyUtils.copyObject(UserUtils.getLoginUser(),UserInfoDTO.class);
response.setContentType(APPLICATION_JSON);
response.getWriter().write(JSON.toJSONString(Result.ok(userInfoDTO)));
log.info("登录成功!");
// 更新用户id和最近登录时间
updateUserInfo();
}
/**
* 异步更新用户
* TODO 使用自己创建的线程池
*/
@Async
public void updateUserInfo() {
UserAuth userAuth = UserAuth.builder()
.id(UserUtils.getLoginUser().getId())
.ipAddress(UserUtils.getLoginUser().getIpAddress())
.ipSource(UserUtils.getLoginUser().getIpSource())
.lastLoginTime(UserUtils.getLoginUser().getLastLoginTime())
.build();
userAuthService.updateById(userAuth);
}
}
4.AuthenticationFailHandlerImpl
认证失败处理器
/**
* 认证失败处理器
* @author chenjiayan
* @date 2022/12/25
*/
@Component
public class AuthenticationFailHandlerImpl implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException{
response.setContentType(APPLICATION_JSON);
response.getWriter().write(JSON.toJSONString(Result.fail(e.getMessage())));
}
}
5.LogoutSuccessHandlerImpl
退出登录处理器
/**
* 退出登录处理器
* @author chenjiayan
* @date 2022/12/25
*/
@Component
public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setContentType(APPLICATION_JSON);
response.getWriter().write(JSON.toJSONString(Result.ok()));
}
}
五、加载数据库中的权限信息
从数据库加载所有路径及其对应的角色信息,和请求的路径进行比对,比对成功把该路径对应的所有角色返回。如果该路径没有对应任何角色,任意返回一个没有的角色,代表该路径不可访问。若请求路径没有匹配任何一个路径,说明可以匿名访问,直接放行。
/**
* 接口拦截规则
* @author chenjiayan
* @date 2022/12/25
*/
@Component
public class FilterInvocationSecurityMetadataSourceImpl implements FilterInvocationSecurityMetadataSource {
/**
* 资源角色列表
*/
private static List<ResourceRoleDTO> resourceRoleList;
@Autowired
private RoleMapper roleMapper;
@PostConstruct // 构造函数执行后执行(初始化)
public void loadDateSource(){
resourceRoleList = roleMapper.listResourceRoles();
}
/**
* 清空接口角色信息
*/
public void clearDataSource(){
resourceRoleList = null;
}
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
// 修改接口角色关系后重新加载
if(CollectionUtils.isEmpty(resourceRoleList)){
this.loadDateSource();
}
FilterInvocation fi = (FilterInvocation) object;
// 获取用户请求方式
String method = fi.getRequest().getMethod();
// 获取用户请求Url
String url = fi.getRequest().getRequestURI();
// 路径匹配
AntPathMatcher antPathMatcher = new AntPathMatcher();
for (ResourceRoleDTO resourceRoleDTO : resourceRoleList) {
if(antPathMatcher.match(resourceRoleDTO.getUrl(),url)&&resourceRoleDTO.getRequestMethod().equals(method)){
List<String> roleList = resourceRoleDTO.getRoleList();
if(CollectionUtils.isEmpty(roleList)){
return SecurityConfig.createList("disable");
}
return SecurityConfig.createList(roleList.toArray(new String[]{}));
}
}
// 方法返回 null 的话,意味着当前这个请求不需要任何角色就能访问,甚至不需要登录。
return null;
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
}
六、访问决策管理器
将访问路径所需的角色和用户拥有的角色进行比对,用户只要拥有任何一个对应的角色即可放行。
/**
* 访问决策管理器
* @author chenjiayan
* @date 2022/12/25
*/
@Component
public class AccessDecisionManagerImpl implements AccessDecisionManager {
/**
* 决策
* @param authentication 用户认证信息
* @param object
* @param configAttributes 权限信息
* @throws AccessDeniedException
* @throws InsufficientAuthenticationException
*/
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
// 获取用户权限列表
List<String> permissionList = authentication.getAuthorities()
.stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList());
for(ConfigAttribute item :configAttributes){
if(permissionList.contains(item.getAttribute())){
// 用户权限包含该操作权限
return ;
}
}
throw new AccessDeniedException("没有操作权限");
}
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
@Override
public boolean supports(Class<?> clazz) {
return true;
}
}
七、从数据库中查找用户信息
从数据库中查找该用户的用户名、密码、拥有的角色等信息。
/**
* @author chenjiayan
* @date 2022/12/25
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserAuthService userAuthService;
@Autowired
private HttpServletRequest request;
@Autowired
private UserInfoService userInfoService;
@Autowired
private RoleMapper roleMapper;
@Autowired
private RedisService redisService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if(StringUtils.isBlank(username)){
throw new BizException("用户名不能为空!");
}
// 查询账号是否存在
UserAuth userAuth = userAuthService.getOne(new LambdaQueryWrapper<UserAuth>()
.select(UserAuth::getId, UserAuth::getUserInfoId, UserAuth::getUsername, UserAuth::getPassword, UserAuth::getLoginType)
.eq(UserAuth::getUsername, username));
if(Objects.isNull(userAuth)){
throw new BizException("用户名不存在!");
}
// 封装登录信息
return convertUserDetail(userAuth, request);
}
/**
* 封装用户登录信息
* @param userAuth 用户账号
* @param request 请求
* @return {@link UserDetails} 用户登录信息
*/
private UserDetails convertUserDetail(UserAuth userAuth, HttpServletRequest request) {
// 查询账号信息
UserInfo userInfo = userInfoService.getById(userAuth.getUserInfoId());
// 查询账号角色信息
List<String> roleList = roleMapper.listRolesByUserInfoId(userInfo.getId());
// 查询账号点赞信息
// 文章
Set<Object> articleLikeSet = redisService.sMembers(ARTICLE_USER_LIKE + userInfo.getId());
// 评论
Set<Object> commentLikeSet = redisService.sMembers(COMMENT_USER_LIKE + userInfo.getId());
// 说说
Set<Object> talkLikeSet = redisService.sMembers(TALK_USER_LIKE + userInfo.getId());
// 获取设备信息
String ipAddress = IpUtils.getIpAddress(request);
String ipSource = IpUtils.getIpSource(ipAddress);
UserAgent userAgent = IpUtils.getUserAgent(request);
// 封装权限集合
return UserDetailDTO.builder()
.id(userAuth.getId())
.loginType(userAuth.getLoginType())
.userInfoId(userInfo.getId())
.username(userAuth.getUsername())
.password(userAuth.getPassword())
.email(userInfo.getEmail())
.roleList(roleList)
.nickname(userInfo.getNickname())
.avatar(userInfo.getAvatar())
.intro(userInfo.getIntro())
.webSite(userInfo.getWebSite())
.articleLikeSet(articleLikeSet)
.commentLikeSet(commentLikeSet)
.talkLikeSet(talkLikeSet)
.ipAddress(ipAddress)
.ipSource(ipSource)
.isDisable(userInfo.getIsDisable())
.os(userAgent.getOperatingSystem().getName())
.lastLoginTime(LocalDateTime.now(ZoneId.of(SHANGHAI.getZone())))
.build();
}
}
八、菜单信息
用户登录后前端请求该用户拥有的菜单信息,后端根据用户的Id进行查询,然后响应给前端。前端根据后端响应的菜单信息显示相应的菜单。完成权限的控制。
因为我还没看到前端,所有这里先不过多介绍,等我看完再进行完善。
补充
security进行认证后会将用户信息存储起来(用ThreadLocal实现),可以在任何地方进行调用,方便使用。
注意:该博客根据 风丶宇个人博客项目 进行编写的,内容中可能出现各种常量和未提及的方法,精力有限请见谅。建议参考源代码进行学习。