springSecurity + JWT如何实现登录
参考博客:https://www.cnblogs.com/RudeCrab/p/14265273.html#权限授权
我对着上面的博客做了简单的spring boot集成springsecurity
https://gitee.com/d-fq/springsecurity.git
1、介绍
Spring Security是一个安全框架,对服务访问提供身份验证和授权。
主要功能:
- 1、认证 (你是谁)
- 2、授权 (你能干什么)
- 3、攻击防护 (防止伪造身份)
登录校验流程:
Spring Security 认证流程:
访问登录接口会经过springSecurity的一套过滤器,我们只需要重点关注两个过滤器即可:
- UsernamePasswordAuthenticationFilter 负责登录认证
- FilterSecurityInterceptor 负责权限授权。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
从引入springSecurity的包开始,Spring Security就默认提供了许多功能将整个应用给保护了起来,在实际开发中,我们一般会自定义一些配置。新建一个配置类,重写WebSecurityConfigurerAdapter
的方法就能对Spring Security进行自定义配置。
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}
2、登录认证
认证流程:
Spring Security中三个核心组件:
Authentication
:存储了认证信息,代表当前登录用户SeucirtyContext
:上下文对象,用来获取AuthenticationSecurityContextHolder
:上下文管理对象,用来在程序任何地方获取SecurityContext
他们关系如下:
Authentication
中那三个玩意就是认证信息:
Principal
:用户信息,没有认证时一般是用户名,认证后一般是用户对象Credentials
:用户凭证,一般是密码Authorities
:用户权限
Spring Security的认证流程主要有2步:
- 1、判断用户的账号密码是否正确
- 2、将
Authentication
放置到SecurityContext
中就完成认证
// 调用service层执行判断业务逻辑
if (!userService.login(用户名, 用户密码)) {
return "账号密码错误";
}
// 账号密码正确了才将认证信息放到上下文中(用户权限需要再从数据库中获取,后面再说,这里省略)
Authentication authentication = new UsernamePasswordAuthenticationToken(用户名, 用户密码, 用户的权限集合);
SecurityContextHolder.getContext().setAuthentication(authentication);
AuthenticationManager认证方式
AuthenticationManager
就是Spring Security用于执行身份验证的组件,只需要调用它的authenticate
方法即可完成认证。Spring Security默认的认证方式就是在UsernamePasswordAuthenticationFilter
这个过滤器中调用这个组件,该过滤器负责认证逻辑。
要想以自定义的方式使用这个组件,先在之前配置类配置一下:
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
}
这里我们写上完整的登录接口代码:
@RestController
@RequestMapping("/API")
public class LoginController {
@Autowired
private AuthenticationManager authenticationManager;
@PostMapping("/login")
public String login(@RequestBody LoginParam param) {
// 生成一个包含账号密码的认证信息
Authentication token = new UsernamePasswordAuthenticationToken(param.getUsername(), param.getPassword());
// AuthenticationManager校验这个认证信息,返回一个已认证的Authentication
Authentication authentication = authenticationManager.authenticate(token);
// 将返回的Authentication存到上下文中
SecurityContextHolder.getContext().setAuthentication(authentication);
return "登录成功";
}
}
AuthenticationManager
的校验提供了3个组件
- 调用
UserDetialsService
接口的loadUserByUsername(String username)方法,通过用户名查询用户对象,默认实现是在内存中查询。 UserDetails
定义 用户对象 的属性,该接口中提供了账号、密码等通用属性。PasswordEncoder
负责密码加密与校验
AuthenticationManager
的部分源码:
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
...省略其他代码
// 传递过来的用户名
String username = authentication.getName();
// 调用UserDetailService的方法,通过用户名查询出用户对象UserDetail(查询不出来UserDetailService则会抛出异常)
UserDetails userDetails = this.getUserDetailsService().loadUserByUsername(username);
String presentedPassword = authentication.getCredentials().toString();
// 传递过来的密码
String password = authentication.getCredentials().toString();
// 使用密码解析器PasswordEncoder传递过来的密码是否和真实的用户密码匹配
if (!passwordEncoder.matches(password, userDetails.getPassword())) {
// 密码错误则抛出异常
throw new BadCredentialsException("错误信息...");
}
// 注意哦,这里返回的已认证Authentication,是将整个UserDetails放进去充当Principal
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(userDetails,
authentication.getCredentials(), userDetails.getAuthorities());
return result;
...省略其他代码
}
UserDetialsService
、UserDetails
、PasswordEncoder
,这三个组件S由我们自己来实现!
加密器PasswordEncoder
首先是PasswordEncoder
,这个接口很简单就两个重要方法:
public interface PasswordEncoder {
/**
* 加密
*/
String encode(CharSequence rawPassword);
/**
* 将未加密的字符串(前端传递过来的密码)和已加密的字符串(数据库中存储的密码)进行校验
*/
boolean matches(CharSequence rawPassword, String encodedPassword);
}
Spring Security提供了很多加密器实现,我们这里选定一个就好。可以在之前所说的配置类里进行如下配置:
@Bean
public PasswordEncoder passwordEncoder() {
// 这里我们使用bcrypt加密算法,安全性比较高
return new BCryptPasswordEncoder();
}
往数据库中添加用户数据时就要将密码进行加密,否则后续进行密码校验时从数据库拿出来的还是明文密码,是无法通过校验的。比如有一个用户注册接口:
@Autowired
private PasswordEncoder passwordEncoder;
@PostMapping("/register")
public String register(@RequestBody LoginParam param) {
UserEntity user = new UserEntity();
// 调用加密器将前端传递过来的密码进行加密
user.setUsername(param.getUsername()).setPassword(passwordEncoder.encode(param.getPassword()));
// 将用户实体对象添加到数据库
userService.save(user);
return "注册成功";
}
用户对象UserDetails
该接口就是我们所说的用户对象,它提供了用户的一些通用属性:
public interface UserDetails extends Serializable {
/**
* 用户权限集合(这个权限对象现在不管它,到权限时我会讲解)
*/
Collection<? extends GrantedAuthority> getAuthorities();
/**
* 用户密码
*/
String getPassword();
/**
* 用户名
*/
String getUsername();
/**
* 用户没过期返回true,反之则false
*/
boolean isAccountNonExpired();
/**
* 用户没锁定返回true,反之则false
*/
boolean isAccountNonLocked();
/**
* 用户凭据(通常为密码)没过期返回true,反之则false
*/
boolean isCredentialsNonExpired();
/**
* 用户是启用状态返回true,反之则false
*/
boolean isEnabled();
}
实际开发中我们一般会自己实现该接口,实现此接口要重写很多方法比较麻烦,我们可以继承Spring Security提供的org.springframework.security.core.userdetails.User类,该类实现了UserDetails接口帮我们省去了重写方法的工作:
public class UserDetail extends User {
/**
* 我们自己的用户实体对象,要调取用户信息时直接获取这个实体对象。(这里我就不写get/set方法了)
*/
private UserEntity userEntity;
public UserDetail(UserEntity userEntity, Collection<? extends GrantedAuthority> authorities) {
// 必须调用父类的构造方法,以初始化用户名、密码、权限
super(userEntity.getUsername(), userEntity.getPassword(), authorities);
this.userEntity = userEntity;
}
}
业务对象UserDetailsService
该接口很简单只有一个方法:
public interface UserDetailsService {
/**
* 根据用户名获取用户对象(获取不到直接抛异常)
*/
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
我们自己的用户业务类实现该接口即可完成自己的逻辑:
@Service
public class UserServiceImpl implements UserService, UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) {
// 从数据库中查询出用户实体对象
UserEntity user = userMapper.selectByUsername(username);
// 若没查询到一定要抛出该异常,这样才能被Spring Security的错误处理器处理
if (user == null) {
throw new UsernameNotFoundException("没有找到该用户");
}
// 走到这代表查询到了实体对象,那就返回我们自定义的UserDetail对象(这里权限暂时放个空集合,后面我会讲解)
return new UserDetail(user, Collections.emptyList());
}
}
认证异常处理器AuthenticationEntryPoint
当我们查询用户失败时或者校验密码失败时都会抛出Spring Security的自定义异常。这些异常不可能放任不管,Spring Security对于这些异常都是在ExceptionTranslationFilter
过滤器中进行处理,而AuthenticationEntryPoint
则专门处理认证异常!
该接口也只有一个方法:
public interface AuthenticationEntryPoint {
/**
* 接收异常并处理
*/
void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException);
}
我们来自定义一个类实现我们自己的错误处理逻辑:
public class MyEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException {
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
// 直接提示前端认证错误
out.write("认证错误");
out.flush();
out.close();
}
}
用户传递过来账号密码 -> 认证校验 -> 异常处理,这一整套流程的组件我们就都给定义完了!现在只差最后一步,就是在Spring Security配置类里面进行一些配置,才能让这些生效。
配置
Spring Security对哪些接口进行保护、什么组件生效、某些功能是否启用等等都需要在配置类中进行配置,注意看代码注释:
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserServiceImpl userDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
// 关闭csrf和frameOptions,如果不关闭会影响前端请求接口(这里不展开细讲了,感兴趣的自行了解)
http.csrf().disable();
http.headers().frameOptions().disable();
// 开启跨域以便前端调用接口
http.cors();
// 这是配置的关键,决定哪些接口开启防护,哪些接口绕过防护
http.authorizeRequests()
// 注意这里,是允许前端跨域联调的一个必要配置
.requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
// 指定某些接口不需要通过验证即可访问。登陆、注册接口肯定是不需要认证的
.antMatchers("/API/login", "/API/register").permitAll()
// 这里意思是其它所有接口需要认证才能访问
.anyRequest().authenticated()
// 指定认证错误处理器
.and().exceptionHandling().authenticationEntryPoint(new MyEntryPoint());
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 指定UserDetailService和加密器
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
其中用的最多的就是configure(HttpSecurity http)方法,可以通过HttpSecurity进行许多配置。当我们重写这个方法时,就已经关闭了默认的表单登录方式
总结和补充
总结一下流程:
- 1、用户调进行登录操作,传递账号密码过来👉登录接口调用
AuthenticationManager
- 2、根据用户名查询出用户数据 ->
UserDetailService
查询出UserDetails
- 3、将传递过来的密码和数据库中的密码进行对比校验 ->
PasswordEncoder
- 4、校验通过则将认证信息存入到上下文中 -> 将
UserDetails
存入到Authentication
,将Authentication
存入到SecurityContext
- 5、如果认证失败则抛出异常 -> 由
AuthenticationEntryPoint
处理
刚才我们讲的认证方式都是基于 session 机制,认证后Spring Security会将 SecurityContext 存入到session中,Key为HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY
。也就是说,你完全可以通过如下方式获取SecurityContext:
SecurityContext securityContext = (SecurityContext)session
.getAttribute(HttpSessionSecurityContextRepository
.SPRING_SECURITY_CONTEXT_KEY)
官方还是不推荐这样直接操作的,因为统一通过SecurityContextHolder
操作更利于管理!使用SecurityContextHolder
除了获取当前用户外,退出登录的操作也是很方便的:
@GetMapping("/logout")
public String logout() {
SecurityContextHolder.clearContext();
return "退出成功";
}
目前登录功能已完成,接下来就是集成JWT
JWT集成
JWT和Session
上面说的认证方式是基于 session 机制,可下面的代码也没看到传递凭证的过程呀,这是因为这些工作Servlet都帮我们做好了!
@PostMapping("/login")
public ResultEntity login(@RequestBody LoginParam param) {
// 生成一个包含账号密码的认证信息
Authentication token = new UsernamePasswordAuthenticationToken(param.getUsername(), param.getPassword());
// AuthenticationManager校验这个认证信息,返回一个已认证的Authentication
Authentication authentication = authenticationManager.authenticate(token);
// 将返回的Authentication存到上下文中
SecurityContextHolder.getContext().setAuthentication(authentication);
return new ResultEntity(0, "操作成功", "登录成功");
}
如果用户第一次访问某个服务器时,服务器响应数据时会在响应头的Set-Cookie标识里将Session Id返回给浏览器,浏览器就将标识中的数据存在Cookie中:
浏览器后续访问服务器就会携带Cookie:
每一个Session Id都对应一个HttpSession对象,然后服务器就根据你这个HttpSession对象来检测你这个客户端是否已经登录了,
JWT的2个主要功能:
- 可以将一段数据加密成一段字符串,也可以从这字符串解密回数据
- 可以对这个字符串进行校验,比如有没有过期,有没有被篡改
有这两个特性之后就可以用来做登录认证了。当用户登录成功的时候,服务器生成一个JWT字符串返回给浏览器,浏览器将JWT保存起来,在之后的请求中都携带上JWT,服务器再对这个JWT进行校验,校验通过的话就代表这个用户登录了。
Session和JWT的区别:Session是有状态的,JWT是无状态的:
Session在服务端保存了用户信息,而JWT在服务端没有保存任何信息。当前端携带Session Id到服务端时,服务端要检查其对应的HttpSession中有没有保存用户信息,保存了就代表登录了。当使用JWT时,服务端只需要对这个字符串进行校验,校验通过就代表登录了。
JWT的引入
先要导入一个依赖项:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
为了方便使用,我们先写一个JWT的工具类,工具类就提供两个方法一个生成一个解析 :
public final class JwtUtil {
/**
* 这个秘钥是防止JWT被篡改的关键,随便写什么都好,但决不能泄露
*/
private final static String secretKey = "whatever";
/**
* 过期时间目前设置成2天,这个配置随业务需求而定
*/
private final static Duration expiration = Duration.ofHours(2);
/**
* 生成JWT
* @param userName 用户名
* @return JWT
*/
public static String generate(String userName) {
// 过期时间
Date expiryDate = new Date(System.currentTimeMillis() + expiration.toMillis());
return Jwts.builder()
.setSubject(userName) // 将userName放进JWT
.setIssuedAt(new Date()) // 设置JWT签发时间
.setExpiration(expiryDate) // 设置过期时间
.signWith(SignatureAlgorithm.HS512, secretKey) // 设置加密算法和秘钥
.compact();
}
/**
* 解析JWT
* @param token JWT字符串
* @return 解析成功返回Claims对象,解析失败返回null
*/
public static Claims parse(String token) {
// 如果是空字符串直接返回null
if (StringUtils.isEmpty(token)) {
return null;
}
// 这个Claims对象包含了许多属性,比如签发时间、过期时间以及存放的数据等
Claims claims = null;
// 解析失败了会抛出异常,所以我们要捕捉一下。token过期、token非法都会导致解析失败
try {
claims = Jwts.parser()
.setSigningKey(secretKey) // 设置秘钥
.parseClaimsJws(token)
.getBody();
} catch (JwtException e) {
// 这里应该用日志输出,为了演示方便就直接打印了
System.err.println("解析失败!");
}
return claims;
}
基本工作完成之后,首先要做的就是在配置类里禁用掉session
:
@Override
protected void configure(HttpSecurity http) throws Exception {
...其它配置这里不写了
// 禁用session
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
再修改一下登录接口,当用户登录成功的同时,我们需要生成token并返回给前端,这样前端才能访问其他接口时携带token
@Autowired
private UserService userService;
@PostMapping("/login")
public UserVO login(@RequestBody @Validated LoginParam user) {
// 调用业务层执行登录操作
return userService.login(user);
}
业务层方法:
@Autowired
PasswordEncoder passwordEncoder;
@Override
public ResultEntity login(LoginParam param) {
UserEntity user = userMapper.selectByUsername(param.getUsername());
// 若没有查到用户 或者 密码校验失败则抛出自定义异常
if (user == null || !passwordEncoder.matches(param.getPassword(), user.getPassword())) {
throw new ApiException("账号密码错误");
}
// 需要返回给前端的VO对象
UserVO userVO = new UserVO();
userVO.setId(user.getId());
userVO.setUsername(user.getUsername());
// 生成JWT,将用户名数据存入其中
userVO.setToken(JwtUtil.generate(user.getUsername()));
return new ResultEntity(1,"操作成功", userVO);
}
我们执行一下登录操作:
可以看到登录成功时接口会返回token,后续我们再访问其它接口时需要将token放到请求头中。这里我们需要自定义一个认证过滤器,来对token进行校验:
@Component
public class LoginFilter extends OncePerRequestFilter {
@Autowired
private JwtUtil jwtManager;
@Autowired
private UserService userService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
// 从请求头中获取token字符串并解析(JwtManager之前文章有详解,这里不多说了)
Claims claims = jwtManager.parse(request.getHeader("Authorization"));
if (claims != null) {
// 从`JWT`中提取出之前存储好的用户名
String username = claims.getSubject();
// 查询出用户对象
UserDetails user = userService.loadUserByUsername(username);
// 手动组装一个认证对象
Authentication authentication = new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities());
// 将认证对象放到上下文中
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.doFilter(request, response);
}
过滤器中的逻辑和之前介绍的最简单的认证方式逻辑是一致的,每当一个请求来时我们都会校验JWT进行认证,上下文对象中有了Authentication后续过滤器就会知道该请求已经认证过了。
这个自定义的认证过滤器需要插入到默认的认证过滤器之前,这样我们的过滤器才能生效,所以需要进行如下配置:
http.addFilterBefore(loginFilter, UsernamePasswordAuthenticationFilter.class);
我们可以断点调试看一下现在的过滤器是怎样的:
可以看到我们自定义的过滤器已经在过滤器链中,因为没有启用表单认证所以UsernamePasswordAuthenticationFilter
被移除了。携带token就可以访问其他接口了
权限授权
来梳理一下接口权限的授权的流程:
- 1、当一个请求过来,我们先得知道这个请求的规则,即需要怎样的权限才能访问
- 2、然后获取当前登录用户所拥有的权限
- 3、再校验当前用户是否拥有该请求的权限
- 4、用户拥有这个权限则正常返回数据,没有权限则拒绝请求
Spring Security的授权发生在FilterSecurityInterceptor过滤器中:
- 1、首先调用的是
SecurityMetadataSource
,来获取当前请求的鉴权规则 - 2、然后通过
Authentication
获取当前登录用户所有权限数据:GrantedAuthority
,这个我们前面提过,认证对象里存放这权限数据 - 3、再调用
AccessDecisionManager
来校验当前用户是否拥有该权限 - 4、如果有就放行接口,没有则抛出异常,该异常会被
AccessDeniedHandler
处理
过滤器里大概的源码:
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
...省略其它代码
// 这是Spring Security封装的对象,该对象里包含了request等信息
FilterInvocation fi = new FilterInvocation(request, response, chain);
// 这里调用了父类的AbstractSecurityInterceptor的方法,认证核心逻辑基本全在父类里
InterceptorStatusToken token = super.beforeInvocation(fi);
...省略其它代码
}
父类的beforeInvocation
大概源码如下:
protected InterceptorStatusToken beforeInvocation(Object object) {
...省略其它代码
// 调用SecurityMetadataSource来获取当前请求的鉴权规则,这个ConfigAttribue就是规则,后面我会讲
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);
// 如果当前请求啥规则也没有,就代表该请求无需授权即可访问,直接结束方法
if (CollectionUtils.isEmpty(attributes)) {
return null;
}
// 获取当前登录用户
Authentication authenticated = authenticateIfRequired();
// 调用AccessDecisionManager来校验当前用户是否拥有该权限,没有权限则抛出异常
this.accessDecisionManager.decide(authenticated, object, attributes);
...省略其它代码
}
接下来自定义这些组件,以完成我们自己的鉴权逻辑。
鉴权规则源SecurityMetadataSource
该接口我们只需要关注一个方法:
public interface SecurityMetadataSource {
/**
* 获取当前请求的鉴权规则
* @param object 该参数就是Spring Security封装的FilterInvocation对象,包含了很多request信息
* @return 鉴权规则对象
*/
Collection<ConfigAttribute> getAttributes(Object object);
}
ConfigAttribute
就是我们所说的鉴权规则,该接口只有一个方法:
public interface ConfigAttribute {
/**
* 这个字符串就是规则,它可以是角色名、权限名、表达式等等。
* 你完全可以按照自己想法来定义,后面AccessDecisionManager会用这个字符串
*/
String getAttribute();
}
我们授权的实现全是靠着资源id,一般用户id关联角色id,角色id关联资源id,这样用户就相当于关联了资源,这里为了简便,用户id直接关联资源id,以资源id作为权限的标记。接下咱们就来自定义SecurityMetadataSource
组件:
@Component
public class MySecurityMetadataSource implements SecurityMetadataSource {
/**
* 当前系统所有接口资源对象,放在这里相当于一个缓存的功能。
* 你可以在应用启动时将该缓存给初始化,也可以在使用过程中加载数据,这里我就不多展开说明了
*/
private static final Set<Resource> RESOURCES = new HashSet<>();
@Override
public Collection<ConfigAttribute> getAttributes(Object object) {
// 该对象是Spring Security帮我们封装好的,可以通过该对象获取request等信息
FilterInvocation filterInvocation = (FilterInvocation) object;
HttpServletRequest request = filterInvocation.getRequest();
// 遍历所有权限资源,以和当前请求进行匹配
for (Resource resource : RESOURCES) {
// 因为我们url资源是这种格式:GET:/API/user/test/{id},冒号前面是请求方法,冒号后面是请求路径,所以要字符串拆分
String[] split = resource.getPath().split(":");
// 因为/API/user/test/{id}这种路径参数不能直接equals来判断请求路径是否匹配,所以需要用Ant类来匹配
AntPathRequestMatcher ant = new AntPathRequestMatcher(split[1]);
// 如果请求方法和请求路径都匹配上了,则代表找到了这个请求所需的权限资源
if (request.getMethod().equals(split[0]) && ant.matches(request)) {
// 将我们权限资源id返回,这个SecurityConfig就是ConfigAttribute一个简单实现
return Collections.singletonList(new SecurityConfig(resource.getId().toString()));
}
}
// 走到这里就代表该请求无需授权即可访问,返回空
return null;
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
// 不用管,这么写就行
return null;
}
@Override
public boolean supports(Class<?> clazz) {
// 不用管,这么写就行
return true;
}
}
注意,我们这里返回的ConfigAttribute鉴权规则,就是我们的资源id。
用户权限GrantedAuthority
该组件代表用户所拥有的权限,和ConfigAttribute一样也只有一个方法,该方法返回的字符串就是代表着权限
public interface GrantedAuthority extends Serializable {
String getAuthority();
}
将GrantedAuthority
和 ConfigAttribute
一对比,就知道用户是否拥有某个权限了。
Spring Security对 GrantedAuthority
有一个简单实现 SimpleGrantedAuthority
,对咱们来说够用了,所以我们额外再新建一个实现。我们要做的就是在UserDetialsService中,获取用户对象的同时也将权限数据查询出来:
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 从数据库中查询出用户实体对象, 这里根据用户名查询,没有对密码进行判断,密码匹配是在authenticate方法进行的
UserEntity user = userMapper.selectByUsername(username);
// 若没查询到一定要抛出该异常,这样才能被Spring Security的错误处理器处理
if (user == null) {
throw new UsernameNotFoundException("没有找到该用户");
}
// 先将该用户所拥有的资源id全部查询出来,再转换成`SimpleGrantedAuthority`权限对象
Set<SimpleGrantedAuthority> authorities = resourceService.getIdsByUserId(user.getId())
.stream()
.map(String::valueOf)
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toSet());
// 走到这代表查询到了实体对象,那就返回我们自定义的UserDetail对象
return new UserDetail(user, authorities);
授权管理AccessDecisionManager
这个组件才最终决定了你有没有某个权限,该接口我们只需关注一个方法:
public interface AccessDecisionManager {
/**
* 授权操作,如果没有权限则抛出异常
*
* @param authentication 当前登录用户,以获取当前用户权限信息
* @param object FilterInvocation对象,以获取request信息
* @param configAttributes 当前请求鉴权规则
*/
void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
throws AccessDeniedException, InsufficientAuthenticationException;
}
该方法接受了这几个参数后完全能做到权限校验了,我们来实现自己的逻辑:
@Component
public class MyDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) {
// 如果授权规则为空则代表此URL无需授权就能访问
if (Collections.isEmpty(configAttributes)) {
return;
}
// 判断授权规则和当前用户所属权限是否匹配
for (ConfigAttribute ca : configAttributes) {
for (GrantedAuthority authority : authentication.getAuthorities()) {
// 如果匹配上了,代表当前登录用户是有该权限的,直接结束方法
if (Objects.equals(authority.getAuthority(), ca.getAttribute())) {
return;
}
}
}
// 走到这里就代表没有权限,必须要抛出异常,否则错误处理器捕捉不到
throw new AccessDeniedException("没有相关权限");
}
@Override
public boolean supports(ConfigAttribute attribute) {
// 不用管,这么写就行
return true;
}
@Override
public boolean supports(Class<?> clazz) {
// 不用管,这么写就行
return true;
}
}
授权错误处理器AccessDeniedHandler
该组件和之前的认证异常处理器一样,只有一个方法用来处理异常,只不过这个是用来处理授权异常的。我们直接来实现:
public class MyDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
out.write("没有相关权限");
out.flush();
out.close();
}
}
配置
组件都定义好了,那我们接下来就是最后一步咯,就是让这些组件生效。我们的鉴权规则源组件SecurityMetadataSource和授权管理组件AccessDecisionManager必须通过授权过滤器FilterSecurityInterceptor来配置生效,所以我们得自己先写一个过滤器,这个过滤器的核心代码基本按照父类的写就行,主要就是属性的配置:
@Component
public class AuthFilter extends AbstractSecurityInterceptor implements Filter {
@Autowired
private SecurityMetadataSource securityMetadataSource;
@Override
public SecurityMetadataSource obtainSecurityMetadataSource() {
// 将我们自定义的SecurityMetadataSource给返回
return this.securityMetadataSource;
}
@Override
@Autowired
public void setAccessDecisionManager(AccessDecisionManager accessDecisionManager) {
// 将我们自定义的AccessDecisionManager给注入
super.setAccessDecisionManager(accessDecisionManager);
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
// 下面的就是按照父类写法写的
FilterInvocation fi = new FilterInvocation(request, response, chain);
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
// 执行下一个拦截器
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
// 请求之后的处理
super.afterInvocation(token, null);
}
}
@Override
public Class<?> getSecureObjectClass() {
return FilterInvocation.class;
}
@Override
public void init(FilterConfig filterConfig) {}
@Override
public void destroy() {}
}
过滤器定义好了,回到Spring Security配置类让这个过滤器插入到原有的鉴权过滤器之前:
http.addFilterBefore(authFilter, FilterSecurityInterceptor.class);
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)