1.相关代码
AuthController
package com.example.netdisk.controller;
import com.example.netdisk.common.Result;
import com.example.netdisk.dto.User.UpDTO;
import com.example.netdisk.entity.SysUser;
import com.example.netdisk.exception.BusinessException;
import com.example.netdisk.security.jwt.JwtUtil;
import com.example.netdisk.service.SysUserService;
import com.example.netdisk.vo.LoginVO;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.*;
/**
* 身份认证
*/
@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
public class AuthController {
private final SysUserService userService;
private final AuthenticationManager authenticationManager;
private final JwtUtil jwtUtil;
/**
* 注册
*/
@PostMapping("/register")
public Result<Void> register(@RequestBody UpDTO dto) {
if(userService.existsByUsername(dto.getUsername())) {
throw new BusinessException(409, "用户名已存在");
}
SysUser user = new SysUser();
user.setUsername(dto.getUsername());
user.setPassword(dto.getPassword());
// 默认给普通用户角色
userService.createUser(user, Collections.singletonList(2L));
return Result.successMsg("注册成功");
}
/**
* 登录
*/
@PostMapping("/login")
public Result<LoginVO> login(@RequestBody UpDTO dto) {
// 触发 Spring Security 的认证流程
// 如果认证失败,这行代码会抛出异常,后面的代码不会执行
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
dto.getUsername(),
dto.getPassword()
)
);
UserDetails userDetails = (UserDetails)authentication.getPrincipal();
Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
List<String> roles = new ArrayList<>();
List<String> perms = new ArrayList<>();
for (GrantedAuthority authority : authorities) {
String s = authority.getAuthority();
if (s != null) {
if (s.startsWith("ROLE_")) {
roles.add(s);
} else {
perms.add(s);
}
}
}
SysUser user = userService.findUserByUsername(dto.getUsername());
// 上面从内存里取,代替这里从数据库取
// List<String> roles = userService.findRolesByUserId(user.getId());
// List<String> perms = userService.findPermsByUserId(user.getId());
LoginVO vo = new LoginVO();
vo.setUserId(user.getId());
vo.setUsername(user.getUsername());
vo.setNickname(user.getNickname());
vo.setStatus(user.getStatus());
vo.setRoles(roles);
vo.setPerms(perms);
String token = jwtUtil.generateToken(dto.getUsername());
vo.setToken(token);
Date expiration = jwtUtil.getExpiration(token);
vo.setExpiration(expiration);
return Result.success(vo);
}
}
SecurityConfig
package com.example.netdisk.security.config;
import com.example.netdisk.security.filter.JwtFilter;
import com.example.netdisk.security.service.CustomUserDetailsService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* 这个 SecurityConfig 类是 Spring Security 的核心配置类
* 它定义了整个应用的安全策略:哪些请求需要认证、使用什么认证方式、密码如何加密等
*/
@Configuration
@RequiredArgsConstructor
@EnableMethodSecurity
public class SecurityConfig {
private final JwtFilter jwtFilter;
// 暴露 AuthenticationManager 作为 Bean(登录用到了)
// Spring Security的核心认证接口,处理认证请求(如登录验证)
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();//拿到默认的 认证管理器
}
/**
* Spring Security 的密码校验逻辑在 DaoAuthenticationProvider
* Spring Boot 的自动配置发现缺少 AuthenticationProvider,
* 会自动创建一个 DaoAuthenticationProvider,然后从容器中找到
* UserDetailsService 和 PasswordEncoder 并注入,
* 流程大概如下
*/
// @Bean
// public DaoAuthenticationProvider authenticationProvider(
// CustomUserDetailsService userDetailsService,
// PasswordEncoder passwordEncoder) {
//
// DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
//
// provider.setUserDetailsService(userDetailsService); // 告诉provider如何查用户
// provider.setPasswordEncoder(passwordEncoder); // 告诉provider如何比对密码
//
// return provider;
// }
/**
* Bean注解:将当前方法的返回值注册为一个 Bean(一个由 Spring 管理的对象)
* JWT认证方式,必须自定义SecurityFilterChain来跳过默认的 (Session认证)
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()// 关闭CSRF保护:JWT认证通常用于REST API,且JWT存在浏览器LocalStorage中,CSRF防护与无状态API不兼容
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)// 设置无状态会话
.and()//返回 HttpSecurity
.authorizeRequests()// 开始配置URL授权(新版可能用 authorizeHttpRequests)
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()// 预检请求放行
.antMatchers("/auth/**").permitAll()// 登录接口所有人都可访问
.anyRequest().authenticated()// 其他所有请求都需要认证
.and()//返回 HttpSecurity
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);// 将自定义的 JwtFilter 插入到过滤器链中
// 为什么放在它之前:因为我们希望先验证JWT,如果JWT有效就直接认证成功,这种情况下免了一次密码登录
// [匿名过滤器] → [JwtFilter] → [UsernamePasswordAuthenticationFilter] → [其他过滤器] → [目标Controller]
return http.build();
}
/**
* 提供密码加密和验证功能
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
/**
* 更详细的授权配置
* .authorizeRequests()
* .antMatchers("/auth/login", "/auth/register", "/public/**").permitAll() // 公共接口
* .antMatchers("/admin/**").hasRole("ADMIN") // 需要ADMIN角色
* .antMatchers("/user/**").hasAnyRole("USER", "ADMIN") // USER或ADMIN角色
* .antMatchers("/api/**").authenticated() // 需要认证
* .anyRequest().denyAll() // 其他所有请求都拒绝
*/
/**
* 默认过滤器链包含(简化版):
* ├── CsrfFilter // CSRF防护 (❌️被禁用了)
* ├── LogoutFilter // 注销处理
* ├── JwtFilter // 自定义JWT验证
* ├── UsernamePasswordAuthenticationFilter // 表单登录
* ├── DefaultLoginPageGeneratingFilter // 生成登录页
* ├── DefaultLogoutPageGeneratingFilter // 生成注销页
* ├── BasicAuthenticationFilter // HTTP Basic认证
* ├── RequestCacheFilter // 请求缓存
* ├── SecurityContextHolderFilter // 安全上下文
* ├── AnonymousAuthenticationFilter // 匿名认证
* ├── SessionManagementFilter // 会话管理
* ├── ExceptionTranslationFilter // 异常转换
* └── FilterSecurityInterceptor // 权限校验
*
* 请求到达
* ↓
* 1. JwtFilter先执行
* ↓ 如果有有效的JWT
* ↓ 创建UsernamePasswordAuthenticationToken
* ↓ 设置到SecurityContext
* ↓
* 2. UsernamePasswordAuthenticationFilter执行
* ↓ 发现SecurityContext中已经有认证信息
* ↓ 跳过用户名密码认证
* ↓
* 3. 请求继续
*/
CustomUserDetailsService
package com.example.netdisk.security.service;
import com.example.netdisk.entity.SysUser;
import com.example.netdisk.mapper.SysUserMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
private final SysUserMapper sysUserMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser user = sysUserMapper.findUserByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用户不存在");
}
List<String> roles = sysUserMapper.findRolesByUserId(user.getId());
List<String> perms = sysUserMapper.findPermsByUserId(user.getId());
List<GrantedAuthority> authorities = new ArrayList<>();
roles.forEach(r -> authorities.add(new SimpleGrantedAuthority(r)));
perms.forEach(p -> authorities.add(new SimpleGrantedAuthority(p)));
return new User(user.getUsername(), user.getPassword(), user.getStatus(), true, true, true, authorities);
}
}
JwtUtil
package com.example.netdisk.security.jwt;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.util.Date;
import java.util.List;
/**
* 这个注解的意义:在需要此类实例化的地方,由 Spring 代替我们手动 new 一个对象
* 相当于手动挡换自动挡
*/
@Component
public class JwtUtil {
// @Value("${jwt.secret}")
private String secret;
//字段初始化的顺序早于依赖注入,这里secret是null,进而抛出异常(方案:用构造函数注入)
// private final SecretKey key = Keys.hmacShaKeyFor(secret.getBytes());
private final SecretKey key;
// 使用构造函数注入
public JwtUtil(@Value("${jwt.secret}") String secret) {
this.secret = secret;
this.key = Keys.hmacShaKeyFor(secret.getBytes());
}
/**
* 签发令牌(登录成功后的"通行证")
* 服务器确认了你的身份(username),现在给你签发一个带有效期的加密令牌,以后你拿着这个令牌来访问,就不用再重复登录了。
*/
public String generateToken(String username) {
return Jwts.builder()// 1. 创建JWT
// 标准字段
.subject(username)// 2. 设置主题(Subject):把用户名存入令牌中,标识这个令牌属于谁
.issuedAt(new Date()) // 令牌的签发时刻(与过期时间配合使用)
.expiration(new Date(System.currentTimeMillis() + 7200_000))// 3. 设置过期时间:令牌有效期2小时
// 自定义字段
// .claim("uid", userId)
.signWith(key)// 4. 签名加密:用服务器的密钥对令牌进行签名,防止被篡改
.compact();// 5. 压缩成最终字符串(例如:eyJhbGciOiJIUzI1NiJ9.eyJz...4qd7...)
}
/**
* 解析验证令牌,获取用户名
* 客户端拿着令牌来访问了,我要验证这个令牌是不是我签发的、有没有被篡改、有没有过期。如果一切正常,取出里面的用户名,确认是谁在访问。
*/
public Claims parseToken(String token) {
return Jwts.parser()
.verifyWith(key)
.build()
.parseSignedClaims(token)
.getPayload();
}
public String getUserName(String token) {
return parseToken(token).getSubject();
}
// public Long getUserId(String token) {
// return parseToken(token).get("uid", Long.class);
// }
public Date getExpiration(String token) {
return parseToken(token).getExpiration();
}
}
/**
* 【生成令牌】
* 用户名"tom"
* → .subject("tom") (存入sub字段)
* → .expiration(明天此时) (存入exp字段)
* → .signWith(密钥) (生成签名)
* → .compact() (打包成: header.payload.signature)
* → 返回: "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0b20iLCJleHAiOjE2Nzg5MDAwMDB9.4qD7X9..."
*
* 【解析令牌】
* 传入: "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0b20iLCJleHAiOjE2Nzg5MDAwMDB9.4qD7X9..."
* → .verifyWith(密钥) (设置验证密钥)
* → .parseSignedClaims(token) (分割、验签、验期)
* ↓ 验证通过
* → .getPayload() (取出载荷部分: {sub:"tom", exp:1678900000})
* → .getSubject() (取出sub的值: "tom")
* → 返回: "tom"
*/
JwtFilter
package com.example.netdisk.security.filter;
import com.example.netdisk.security.jwt.JwtUtil;
import com.example.netdisk.security.service.CustomUserDetailsService;
import lombok.RequiredArgsConstructor;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* OncePerRequestFilter:确保在一个外部请求的处理过程中,本过滤器只会被执行一次。
*/
@Component
@RequiredArgsConstructor
public class JwtFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final CustomUserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException {
String header = request.getHeader("Authorization");
if(header != null && header.startsWith("Bearer ")) {
String token = header.substring(7);
String username = jwtUtil.getUserName(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
// 将认证信息绑定到当前线程的上下文中
SecurityContextHolder.getContext().setAuthentication(auth);
}
// 放行请求,继续执行后续的过滤器或目标 Controller
filterChain.doFilter(request, response);
}
}
/**
* 登录:
*
* 1. 用户 POST /auth/login (用户名+密码)
* ↓
* 2. 请求到达,JwtFilter检查Authorization头 → 没有令牌
* ↓
* 3. 请求继续传递到登录Controller
* ↓
* 4. 登录Controller验证用户名密码(用PasswordEncoder比对)
* ↓
* 5. 验证成功 → 调用JwtUtil生成令牌 → 返回给客户端
*/
/**
* 登录后访问受保护的资源:
*
* 1. 用户 GET /api/user/info (携带 Bearer token)
* ↓
* 2. 请求到达JwtFilter
* ↓
* 3. 解析token → 获取用户名 → 加载用户信息 → 存入SecurityContext
* ↓
* 4. 请求继续传递
* ↓
* 5. 后续过滤器检查SecurityContext → 已有认证信息 → 允许访问
* ↓
* 6. 到达目标Controller
*/
SysUserController
package com.example.netdisk.controller;
import com.example.netdisk.common.PageResult;
import com.example.netdisk.common.Result;
import com.example.netdisk.dto.User.UserCreateDTO;
import com.example.netdisk.entity.SysUser;
import com.example.netdisk.service.SysUserService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 用户管理
*/
@RestController
@RequestMapping("/user")
@RequiredArgsConstructor
public class SysUserController {
private final SysUserService userService;
/**
* 创建用户
*/
@PostMapping
@PreAuthorize("hasAuthority('user:add')")
public void create(@RequestBody UserCreateDTO dto) {
userService.createUser(dto.getUser(), dto.getRoleIds());
}
/**
* 删除用户
*
* 检查当前请求的发送者是否有 user:delete 权限,
* 是从 SecurityContext 中获取的
*/
@DeleteMapping("/{id}")
@PreAuthorize("hasAuthority('user:delete')")
public void delete(@PathVariable Long id) {
userService.deleteUser(id);
}
/**
* 给用户分配角色
*/
@PutMapping("/{id}/roles")
@PreAuthorize("hasAuthority('user:add')")
public void assignRoles(@PathVariable Long id, @RequestBody List<Long> roleIds) {
userService.assignRoles(id, roleIds);
}
/**
* 查询所有用户
*/
@GetMapping
public List<SysUser> list() {
return userService.list();
}
/**
* 分页查询用户
*/
@GetMapping("/page")
@PreAuthorize("hasRole('ADMIN')")
public Result<PageResult<SysUser>> page(@RequestParam int pageNum, @RequestParam int pageSize) {
return Result.success(userService.page(pageNum, pageSize));
}
}
2.关系图
3.项目启动后流程
Spring启动过程:
- 找到启动类 NetdiskApplication
- 读取 @SpringBootApplication
- 发现其中包含 @ComponentScan
- 扫描包:com.example.netdisk 及其子包
- 扫描到:
- @RestController
- @Controller
- @Service
- @Component
- @Configuration
- 把这些类注册为 Bean
其中,扫描到@Configuration时,就有SecurityConfig,就会执行@Bean方法,这里就会把自定义的JwtFilter放到SecurityFilterChain里UsernamePasswordAuthenticationFilter的前面。
package com.example.netdisk;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
/**
* @SpringBootApplication = @SpringBootConfiguration + @EnableAutoConfiguration + @ComponentScan
* @SpringBootConfiguration
* @EnableAutoConfiguration
* @ComponentScan: 没写 basePackages,意味着默认扫描“当前类所在包 + 所有子包”
*
* Spring启动过程:
* 1. 找到启动类 NetdiskApplication
* 2. 读取 @SpringBootApplication
* 3. 发现其中包含 @ComponentScan
* 4. 扫描包:com.example.netdisk 及其子包
* 5. 扫描到:
* - @RestController
* - @Controller
* - @Service
* - @Component
* - @Configuration
* 6. 把这些类注册为 Bean
*/
@SpringBootApplication
@EnableScheduling
/**
* 报错:Parameter 0 of constructor in com.example.netdisk.controller.SysUserController
* required a bean of type 'com.example.netdisk.mapper.SysUserMapper'
* that could not be found.
*
* 翻译:未在容器中找到UserMapper的Bean
*
* 解决:@MapperScan("com.example.netdisk.mapper")
*
* 启动时扫描 com.example.netdisk.mapper 包下所有 Mapper 接口
* 并把它们注册成 Spring Bean
* 就可以在Service里直接注入(@Autowired)使用
*/
@MapperScan("com.example.netdisk.mapper")
public class NetdiskApplication {
public static void main(String[] args) {
SpringApplication.run(NetdiskApplication.class, args);
}
}
4.请求处理流程
4-1.登录请求处理流程
- 前端调用
/auth/login接口,经过SecurityFilterChain,没发现token,进入Controller,触发AuthController的login方法
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
dto.getUsername(),
dto.getPassword()
)
);
new UsernamePasswordAuthenticationToken(dto.getUsername(), dto.getPassword())创建一个未认证的令牌对象(包含用户提交的原始凭证——用户名与密码)authenticationManager.authenticate(...)委托 认证管理器(AuthenticationManager) 进行验证,AuthenticationManager来自SecurityConfig,它会依据开发人员配置的 认证提供者(AuthenticationProvider) 去验证凭证。
Spring Boot的自动配置发现缺少 AuthenticationProvider,会自动创建一个 DaoAuthenticationProvider,然后从容器中找到 UserDetailsService(来自CustomUserDetailsService) 和 PasswordEncoder(来自SecurityConfig) 并注入,前者负责告诉 provider 如何通过用户名查找到对应的用户信息,后者负责告诉 provider 如何对比密码是否一致。大致流程如下:
@Bean
public DaoAuthenticationProvider authenticationProvider(
CustomUserDetailsService userDetailsService,
PasswordEncoder passwordEncoder) {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService); // 告诉provider如何查用户
provider.setPasswordEncoder(passwordEncoder); // 告诉provider如何比对密码
return provider;
}
- 如果认证失败,则抛出异常
org.springframework.security.authentication.BadCredentialsException: 用户名或密码错误
at org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider.authenticate(AbstractUserDetailsAuthenticationProvider.java:141)
at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:182)
at com.example.netdisk.controller.AuthController.login(AuthController.java:63)
- 如果认证通过,则返回一个已认证的令牌(authentication),包含了用户的详细信息(UserDetails)、权限列表(Authorities)等,底层执行过程如下:
用户提交凭证
↓
UsernamePasswordAuthenticationToken(未认证)
↓
AuthenticationManager
↓
DaoAuthenticationProvider
↓
UserDetailsService.loadUserByUsername() → 从数据库加载用户信息
↓
PasswordEncoder.matches() → 验证密码是否匹配
↓
UsernamePasswordAuthenticationToken(已认证,包含权限)
↓
返回给调用方
UserDetails userDetails = (UserDetails)authentication.getPrincipal();这里getPrincipal()返回的是UserDetails对象(CustomUserDetailsService中的loadUserByUsername方法返回的User对象),包含了用户名、密码、状态、权限集合等信息。Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();单独拿出权限集合,里面包括角色与权限- 从权限集合里提取出当前登录用户的角色与权限,存到roles与perms数组里
SysUser user = userService.findUserByUsername(dto.getUsername());根据用户名查询用户全部信息,虽然前面 UserDetails 里有用户信息,但只有用户名、密码、状态、权限集合,没有其他信息,列如昵称、性别、年龄等- 构建前端响应对象,填充各个属性
String token = jwtUtil.generateToken(dto.getUsername());根据用户名生成tokenDate expiration = jwtUtil.getExpiration(token);获取token的过期时间- 将token与其过期时间设置到前端响应对象的属性中,使用统一的响应格式
Result封装前端响应对象并返回
响应示例:
{
"code": 200,
"msg": "success",
"data": {
"userId": 1,
"username": "admin",
"nickname": null,
"status": true,
"token": "eyJhbGciOiJIUzI1NiIs...",
"expiration": "2026-08-05T12:11:09.000+00:00",
"roles": [
"ROLE_ADMIN"
],
"perms": [
"user:add",
"user:delete",
"user:view"
]
}
}
4-2.需认证角色/权限的请求的处理流程
- 前端调用
/user/page接口,经过SecurityFilterChain,在自定义的JwtFilter中解析token,拿到用户名,加载权限角色等信息,存入SecurityContext,然后进入Controller,触发SysUserController的page方法. - 由于
SecurityConfig里写了@EnableMethodSecurity,Spring 会为带@PreAuthorize的方法创建代理,在执行page方法前会执行:
MethodSecurityInterceptor
↓
PreInvocationAuthorizationAdvice
↓
ExpressionHandler
3. 开始权限表达式计算:hasRole('ADMIN')内部转换为hasAuthority('ROLE_ADMIN'),然后从SecurityContext中拿到authentication,执行authentication.getAuthorities()拿到authorities,表达式判断ROLE_ADMIN ∈ authorities ? .
- 如果是true,则执行对应方法;如果是false,则抛出异常
AccessDeniedException被(实则不会被ExceptionTranslationFilter捕获,返回403 ForbiddenExceptionTranslationFilter捕获,因为这里已经过了Spring Security过滤器链了,到达Controller了,而ExceptionTranslationFilter只能捕获发生在Spring Security过滤器链中的异常).
比较过程发生在内存中,唯一查询数据库的地方是拿Authorities,也有方案是把Authorities放到token中,就省去了数据库查询。权限放在token里的缺点是,修改权限不会立刻生效,需等token过期才能生效。也有方案把权限放在redis里,以减少对于数据库的查询。
5.补充
如果权限放在token里,岂不是前端可以伪造token来修改权限?
其实不然,首先token是由三部分组成的:Header.Payload.Signature
Payload是base64编码的,可以伪造:
但是后面的Signature是用于校验的(校验发生在JwtUtil中的parseToken方法中的parseSignedClaims方法,会验证签名是否正确、是否过期等),signature = HMACSHA256( base64(header) + "." + base64(payload), secret-key ),前端修改权限意味着修改payload,然后这时的token是Header.新Payload.旧Signature,服务端按照HMACSHA256( base64(header) + "." + base64(新payload), secret-key )计算出来的signature跟token里的旧signature不一致(明显计算公式里的参数变了,即payload变了),所以认为该token无效。所以前端无法伪造token来修改权限。当然这需要保证签名算法(如HS256)可靠,以及密钥(secret-key)不泄露。