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實作),可以在任何地方進行調用,友善使用。
注意:該部落格根據 風丶宇個人部落格項目 進行編寫的,内容中可能出現各種常量和未提及的方法,精力有限請見諒。建議參考源代碼進行學習。