SpringSecurity組件可以為服務提供安全管理的能力,比如身份驗證、授權和針對常見攻擊的保護,是保護基於spring應用程式的事實上的標準; ...
目錄
標簽:Security.登錄.許可權;
一、簡介
SpringSecurity組件可以為服務提供安全管理的能力,比如身份驗證、授權和針對常見攻擊的保護,是保護基於spring應用程式的事實上的標準;
在實際開發中,最常用的是登錄驗證和許可權體系兩大功能,在登錄時完成身份的驗證,載入相關信息和角色許可權,在訪問其他系統資源時,進行許可權的驗證,保護系統的安全;
二、工程搭建
1、工程結構
2、依賴管理
在starter-security
依賴中,實際上是依賴spring-security
組件的6.1.1
版本,對於該框架的使用,主要是通過自定義配置類進行控制;
<!-- 安全組件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>${spring-boot.version}</version>
</dependency>
三、配置管理
1、核心配置類
在該類中涉及到的配置非常多,主要是服務的攔截控制,身份認證的處理流程以及過濾器等,很多自定義的處理類通過該配置進行載入;
@EnableWebSecurity
@EnableMethodSecurity
@Configuration
public class SecurityConfig {
/**
* 基礎配置
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
// 配置攔截規則
httpSecurity.authorizeHttpRequests(authorizeHttpRequests->{
authorizeHttpRequests
.requestMatchers(WhiteConfig.whiteList()).permitAll()
.anyRequest().authenticated();
});
// 禁用預設的登錄和退出
httpSecurity.formLogin(AbstractHttpConfigurer::disable);
httpSecurity.logout(AbstractHttpConfigurer::disable);
httpSecurity.csrf(AbstractHttpConfigurer::disable);
// 異常時認證處理流程
httpSecurity.exceptionHandling(exeConfig -> {
exeConfig.authenticationEntryPoint(authenticationEntryPoint());
});
// 添加過濾器
httpSecurity.addFilterAt(authTokenFilter(),CsrfFilter.class);
return httpSecurity.build() ;
}
@Bean
public BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationEntryPoint authenticationEntryPoint() {
return new AuthExeHandler();
}
@Bean
public OncePerRequestFilter authTokenFilter () {
return new AuthTokenFilter();
}
/**
* 認證管理
*/
@Bean
public AuthenticationManager authenticationManager() {
return new ProviderManager(authenticationProvider()) ;
}
/**
* 自定義用戶認證流
*/
@Bean
public AbstractUserDetailsAuthenticationProvider authenticationProvider() {
return new AuthProvider() ;
}
}
2、認證數據源
UserDetailsService
是載入用戶特定數據的核心介面,編寫用戶服務類並實現該介面,提供用戶信息和許可權體系的數據查詢和載入,作為用戶身份識別的關鍵憑據;
@Service
public class UserService implements UserDetailsService {
@Resource
private UserBaseMapper userBaseMapper;
@Resource
private BCryptPasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
UserBase queryUser = geyByUserName(userName);
if (Objects.isNull(queryUser)){
throw new AuthException("該用戶不存在");
}
List<GrantedAuthority> grantedAuthorityList = new ArrayList<>() ;
grantedAuthorityList.add(new SimpleGrantedAuthority(queryUser.getUserRole())) ;
return new User(queryUser.getUserName(),queryUser.getPassWord(),grantedAuthorityList);
}
public int register (UserBase userBase){
if (!Objects.isNull(userBase)){
userBase.setPassWord(passwordEncoder.encode(userBase.getPassWord()));
userBase.setCreateTime(new Date()) ;
return userBaseMapper.insert(userBase) ;
}
return 0 ;
}
public UserBase getById (Integer id){
return userBaseMapper.selectById(id) ;
}
public UserBase geyByUserName (String userName){
List<UserBase> userBaseList = new LambdaQueryChainWrapper<>(userBaseMapper)
.eq(UserBase::getUserName,userName).last("limit 1").list();
if (userBaseList.size() > 0){
return userBaseList.get(0) ;
}
return null ;
}
}
3、認證流程
自定義用戶名和密碼的身份令牌認證邏輯,基於用戶名Username
從上面的用戶服務類中載入數據並校驗,在驗證成功後將用戶的身份令牌返回給調用者;
@Component
public class AuthProvider extends AbstractUserDetailsAuthenticationProvider {
private static final Logger log = LoggerFactory.getLogger(AuthProvider.class);
@Resource
private UserService userService;
@Resource
private BCryptPasswordEncoder passwordEncoder;
@Override
protected void additionalAuthenticationChecks(
UserDetails userDetails, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
User user = (User) userDetails;
String loginPassword = authentication.getCredentials().toString();
log.info("user:{},loginPassword:{}",user.getPassword(),loginPassword);
if (!passwordEncoder.matches(loginPassword, user.getPassword())) {
throw new AuthException("賬號或密碼錯誤");
}
authentication.setDetails(user);
}
@Override
protected UserDetails retrieveUser(
String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
log.info("username:{}",username);
return userService.loadUserByUsername(username);
}
}
4、身份過濾器
通過繼承OncePerRequestFilter
抽象類,實現用戶身份的過濾器,如果不是白名單請求,需要驗證令牌是否正確有效,SecurityContextHolder
預設狀態下使用ThreadLocal
存儲信息;
@Component
public class AuthTokenFilter extends OncePerRequestFilter {
@Resource
private AuthTokenService authTokenService ;
@Resource
private AuthExeHandler authExeHandler ;
@Override
protected void doFilterInternal(@Nonnull HttpServletRequest request,
@Nonnull HttpServletResponse response,
@Nonnull FilterChain filterChain) throws ServletException, IOException {
String uri = request.getRequestURI();
if (Arrays.asList(WhiteConfig.whiteList()).contains(uri)){
// 如果是白名單直接放行
filterChain.doFilter(request,response);
} else {
String token = request.getHeader("Auth-Token");
if (Objects.isNull(token) || token.isEmpty()){
// Token不存在,攔截返回
authExeHandler.commence(request,response,null);
} else {
Object object = authTokenService.getToken(token);
if (!Objects.isNull(object) && object instanceof User user){
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(user, null,user.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(request,response);
} else {
// Token驗證失敗,攔截返回
authExeHandler.commence(request,response,null);
}
}
}
}
}
四、核心功能
1、登錄退出
自定義登錄和退出兩個介面,基於用戶名和密碼執行上述的身份認證流程,如果認證成功則返回用戶的身份令牌,在請求「非」白名單介面時需要在請求頭中Auth-Token:token
攜帶該令牌,在退出時會清除身份信息;
@Service
public class LoginService {
private static final Logger log = LoggerFactory.getLogger(LoginService.class);
@Resource
private AuthTokenService authTokenService ;
@Resource
private AuthenticationManager authenticationManager;
public String doLogin (UserBase userBase){
AbstractAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
userBase.getUserName().trim(), userBase.getPassWord().trim());
Authentication authentication = authenticationManager.authenticate(authToken) ;
User user = (User) authentication.getDetails();
return authTokenService.createToken(user) ;
}
public Boolean doLogout (String authToken){
SecurityContextHolder.clearContext();
return authTokenService.deleteToken(authToken) ;
}
}
@Service
public class AuthTokenService {
private static final Logger log = LoggerFactory.getLogger(AuthTokenService.class);
@Resource
private RedisTemplate<String,Object> redisTemplate ;
public String createToken (User user){
String userName = user.getUsername();
String token = DigestUtils.md5DigestAsHex(userName.getBytes());
log.info("user-name:{},create-token:{}",userName,token);
redisTemplate.opsForValue().set(token,user,10, TimeUnit.MINUTES);
return token ;
}
public Object getToken (String token){
return redisTemplate.opsForValue().get(token);
}
public Boolean deleteToken (String token){
return redisTemplate.delete(token);
}
}
2、許可權校驗
UserWeb
類中提供用戶的註冊介面,在用戶表中創建兩個測試用戶:admin
對應ROLE_Admin
角色,user
對應ROLE_User
角色,驗證如下幾個介面的許可權控制;
select
介面不需要鑒權,攔截器放行即可訪問;getUser
介面校驗ROLE_User
角色;getAdmin
介面校驗ROLE_Admin
角色;query
介面校驗兩個角色中的任意一個即可;
兩個不同用戶登錄獲取到各自的身份令牌,使用不同的令牌請求介面,在PreAuthorize
驗證通過後才可以正常訪問;
@RestController
public class UserWeb {
@Resource
private UserService userService ;
@PostMapping("/register")
public String register (@RequestBody UserBase userBase){
return "register-"+userService.register(userBase) ;
}
@GetMapping("/select/{id}")
public UserBase select (@PathVariable Integer id){
return userService.getById(id) ;
}
@PreAuthorize("hasRole('User')")
@GetMapping("/user/{id}")
public UserBase getUser (@PathVariable Integer id){
return userService.getById(id) ;
}
@PreAuthorize("hasRole('Admin')")
@GetMapping("/admin/{id}")
public UserBase getAdmin (@PathVariable Integer id){
return userService.getById(id) ;
}
@PreAuthorize("hasAnyRole('User','Admin')")
@GetMapping("/query/{id}")
public UserBase query (@PathVariable Integer id){
return userService.getById(id) ;
}
}
五、參考源碼
文檔倉庫:
https://gitee.com/cicadasmile/butte-java-note
源碼倉庫:
https://gitee.com/cicadasmile/butte-spring-parent
Gitee主頁: https://gitee.com/cicadasmile/butte-java-note