SpringBoot integrates SpringSecurity (advanced)
The "Feng Yu Personal Blog" project uses Spring Security to manage authentication and authorization. The content of SpringSecurity is very large. If you want to fully control it, it is not enough to just read this article. It is recommended to read SpringSecurity by bad programmers at station B. The content is more comprehensive. I would like to call it the strongest SpringSecurity tutorial at station B.
Let's start to introduce how to use it in the project:
1. Database
As the saying goes, it is difficult to finish the work at the beginning, so it is necessary to build the database. Here I will not give the table creation statement, but I will give the relationship diagram of the table, and you can build the table according to your own situation.
2. Introducing dependencies
Introduce spring security dependencies
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Three, Spring Security configuration
First create WebSecurityConfig inherits WebSecurityConfigurerAdapter, which contains most of the configuration of 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()); // 自定义会话存储
}
}
Four, various processors
1.AuthenticationEntryPointImpl
Unauthenticated processing
/**
* 用户未登录处理
* @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
Insufficient Privilege Processor
/**
* 用户权限不通过处理
*
* @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
Authentication success handler
/**
* 登录成功处理器
* @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
authentication failure handler
/**
* 认证失败处理器
* @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
logout handler
/**
* 退出登录处理器
* @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()));
}
}
5. Load the permission information in the database
Load all paths and their corresponding role information from the database, compare with the requested path, and return all the roles corresponding to the path if the comparison is successful. If the path does not correspond to any role, arbitrarily returns a role without it, which means the path is inaccessible. If the request path does not match any path, it means that the request can be accessed anonymously and allowed directly.
/**
* 接口拦截规则
* @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);
}
}
6. Access Decision Manager
Compare the roles required by the access path with the roles owned by the user, and the user can pass as long as he has any corresponding role.
/**
* 访问决策管理器
* @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;
}
}
7. Find user information from the database
Find the user's username, password, roles and other information from the database.
/**
* @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();
}
}
8. Menu information
After the user logs in, the front end requests the menu information owned by the user, and the back end queries according to the user's Id, and then responds to the front end. The front end displays the corresponding menu according to the menu information responded by the back end. Complete authority control.
Because I haven't seen the front end yet, so I won't introduce too much here, and I will improve it after I read it.
Replenish
After the security is authenticated, the user information will be stored (implemented with ThreadLocal), which can be called anywhere for easy use.
(UserDetailDTO) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
注意:该博客根据 风丶宇个人博客项目 进行编写的,内容中可能出现各种常量和未提及的方法,精力有限请见谅。建议参考源代码进行学习。