标签: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