SpringSecurity
Spring Security(旧)
SpringSecurity是一个强大的可高度定制的认证和授权框架,对于Spring应用来说它是一套Web安全标准
流程

使用
导入坐标
1
2
3
4
5
6
7
8
9
10
11<!--SpringSecurity-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--JWT-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>编写SpringSecurity配置类
主要配置:需要鉴权的访问路径、获取用户信息方式、密码认证方式、认证token方式、自定义未授权和未登录结果的返回
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Lazy // 防止嵌套
@Autowired
private IUmsAdminService adminService;
@Resource
private RestfulAccessDeniedHandler restfulAccessDeniedHandler;
@Resource
private RestAuthenticationEntryPoint restAuthenticationEntryPoint;
// 用于配置需要拦截的url路径、jwt过滤器及出异常后的处理器
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.csrf()
.disable()
.sessionManagement()// 基于token,所以不需要session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers(HttpMethod.GET, // 允许对于网站静态资源的无授权访问
"/",
"/*.html",
"/favicon.ico",
"/**/*.html",
"/**/*.css",
"/**/*.js",
"/swagger-resources/**",
"/v2/api-docs/**"
)
.permitAll()
.antMatchers("/admin/login", "/admin/register")// 对登录注册要允许匿名访问
.permitAll()
.antMatchers(HttpMethod.OPTIONS)//跨域请求会先进行一次options请求
.permitAll()
// .antMatchers("/**")//测试时全部运行访问
// .permitAll()
.anyRequest()// 除上面外的所有请求全部需要鉴权认证
.authenticated();
// 禁用缓存
httpSecurity.headers().cacheControl();
// 添加JWT filter
httpSecurity.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
// 添加自定义未授权和未登录结果返回
httpSecurity.exceptionHandling()
.accessDeniedHandler(restfulAccessDeniedHandler)
.authenticationEntryPoint(restAuthenticationEntryPoint);
}
// 用于配置UserDetailsService及PasswordEncoder
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService())
.passwordEncoder(passwordEncoder());
}
// SpringSecurity定义的用于对密码进行编码及比对的接口
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 用于根据用户名获取用户信息
@Bean
public UserDetailsService userDetailsService() {
//获取登录用户信息
return username -> {
UmsAdmin admin = adminService.getAdminByUsername(username);
if (admin != null) {
List<UmsPermission> permissionList = adminService.getPermissionList(admin.getId());
return new AdminUserDetails(admin,permissionList);
}
throw new UsernameNotFoundException("用户名或密码错误");
};
}
// 在用户名和密码校验前添加的过滤器
@Bean
public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter(){
return new JwtAuthenticationTokenFilter();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}1
2
3
4
5
6
7
8
9
10
11
12
13/**
* 当访问接口没有权限时,自定义的返回结果
*/
@Component
public class RestfulAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
response.getWriter().println(JSONUtil.parse(CommonResult.forbidden(e.getMessage())));
response.getWriter().flush();
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14/**
* 当未登录或者token失效访问接口时,自定义的返回结果
*/
@Component
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
response.getWriter().println(JSONUtil.parse(CommonResult.unauthorized(authException.getMessage())));
response.getWriter().flush();
}
}自定义的用户信息类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48public class AdminUserDetails implements UserDetails {
private UmsAdmin umsAdmin;
private List<UmsPermission> permissionList;
public AdminUserDetails(UmsAdmin umsAdmin, List<UmsPermission> permissionList) {
this.umsAdmin = umsAdmin;
this.permissionList = permissionList;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// 获取当前用户权限
return permissionList.stream()
.filter(permissionList -> permissionList.getValue() != null)
.map(permission -> new SimpleGrantedAuthority(permission.getValue()))
.collect(Collectors.toList());
}
@Override
public String getPassword() {
return umsAdmin.getPassword();
}
@Override
public String getUsername() {
return umsAdmin.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return umsAdmin.getStatus().equals(1);
}
}验证token
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class);
@Resource
private UserDetailsService userDetailsService;
@Resource
private JwtTokenUtil jwtTokenUtil;
@Value("${jwt.tokenHeader}")
private String tokenHeader; // Authorization
@Value("${jwt.tokenHead}")
private String tokenHead; // Bearer
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
String authHeader = request.getHeader(this.tokenHeader);
if (authHeader != null && authHeader.startsWith(this.tokenHead)) {
String authToken = authHeader.substring(this.tokenHead.length());// The part after "Bearer "
String username = jwtTokenUtil.getUserNameFromToken(authToken);
LOGGER.info("checking username:{}", username);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.validateToken(authToken, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
LOGGER.info("authenticated user:{}", username);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
chain.doFilter(request, response);
}
}使用
1
2// 在需要鉴权的方法上添加
@PreAuthorize("hasAuthority('pms:brand:read')")注意
如果使用了swagger
在配置Docket时
securitySchemes要传入需要登录认证的路径,否则不会主动带上头信息1
2
3
4
5
6
7
8private List<SecurityContext> securityContexts() {
//设置需要登录认证的路径
List<SecurityContext> result = new ArrayList<>();
result.add(getContextByPath("/brand/.*"));
result.add(getContextByPath("/admin/.*"));
result.add(getContextByPath("/esProduct/.*"));
return result;
}
实例
场景:前端传来用户名和密码,后台认证后返回token并把用户信息存到redis中
1 | |
1 | |
登录
流程
Entity
User,映射数据库表
1 | |
实现UserDetails
1 | |
BlogUserLoginVo,返回前端的Vo
1 | |
1 | |
实现UserDetailsService
用于查询用户信息,返回UserDetails,被AuthenticationManager所调用
1 | |
SpringSecurity配置类
1 | |
Service层
通过身份认证器进行身份验证
1 | |
Controller层
1 | |
认证
每次发送请求时都会在响应头中带上token,通过解析token来比对redis中缓存的登录信息来认证
认证过滤器
工具类
1 | |
解析token
1 | |
添加过滤器
1 | |
自定义返回结果
1 | |
1 | |
添加到配置类中
1 | |
全局异常处理
这里跟SpringSecurity无关
自定义异常
1 | |
全局异常处理器
对Controller的增强
1 | |
登出
根据token删除redis中的登录信息
配置类
1 | |
Service层
1 | |
Controller层
1 | |
鉴权
开启
在启动类上添加注解,开启先/后鉴权的注解。开启后可以使用@PreAuthorize()和@PostAuthorize()
1 | |
查询权限
在获取时将用户信息和权限包装成UserDetail一起返回
1 | |
鉴权
编写鉴权逻辑,并作为Service注入Bean
1 | |
使用
在需要鉴权的接口上添加注解,@ps.hasPermission()为自定义的鉴权方法
1 | |
Spring Security(新)
使用的版本:Spring Boot 3.5.6
使用
下面的代码只是作为认证和授权功能的参考,很多地方还不完善,特别是JwtAuthenticationTokenFilter及Token的存储相关的地方,根据需要进行修改
引入依赖
1 | |
创建Spring Security的配置类
1 | |
用户认证
基于内存的用户认证
下面为在
SecurityConfig中的方法1
2
3
4
5
6
7
8
9
10
11
12
13@Bean
public UserDetailsService userDetailsService() {
// 创建基于内存的用户信息管理器
// manager通过loadUserByUsername方法来获取内存中用户的密码进行匹配
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
// 创建UserDetails对象,用于管理用户名、用户密码、用户角色、用户权限等内容
manager.createUser(
User.withDefaultPasswordEncoder().username("user").password("user123").roles("USER").build()
);
// 如果自己配置的有账号密码, 那么上面讲的 user 和 随机字符串 的默认密码就不能用了
return manager;
}基于mysql数据库的用户认证
实现
UserDetailsService接口用于获取数据库中的用户信息User只包含了账号密码,UserMapper只包含了通过用户名或id获取信息的方法,UserDetailsImpl是一个实现了UserDetails接口的对User的包装类,一看就懂这里省略
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Resource
private UserMapper userMapper;
// loadUserByUsername: 用于获取 User 并用这个 User 来判断用户输入的密码是否正确
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.getUserByUsername(username);
if(user==null){
throw new UsernameNotFoundException(username);
}
return new UserDetailsImpl(user);
}
}继承
OncePerRequestFilter类用于解析JWTOncePerRequestFilter是 Spring Security 提供的一个抽象类,确保在每个请求中只执行一次特定的过滤逻辑。它是实现自定义过滤器的基础,通常用于对请求进行预处理或后处理JWT工具类:生成和解析JWT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51public class JwtUtil {
public static final long JWT_TTL = 60 * 60 * 1000L * 24 * 14; // 有效期14天
public static final String JWT_KEY = "SDFGjhdsfalshdfHFdsjkdsfds121232131afasdfac";
// 生成JWT的唯一标识
public static String getUUID() {
return UUID.randomUUID().toString().replaceAll("-", "");
}
// 生成JWT
public static String createJWT(String subject) {
JwtBuilder builder = getJwtBuilder(subject, null, getUUID());
return builder.compact();
}
// 构建 JWT, 指定签名密钥、签发者、签发时间、过期时间
private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
SecretKey secretKey = generalKey();
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
if (ttlMillis == null) {
ttlMillis = JwtUtil.JWT_TTL;
}
long expMillis = nowMillis + ttlMillis;
Date expDate = new Date(expMillis);
return Jwts.builder()
.id(uuid) // 设置 JWT 的唯一标识
.subject(subject) // 会往载荷中添加"sub": subject,如果要添加其他载荷,可claim(key, value)
.issuer("xw") // 设置 JWT 的发行者
.issuedAt(now) // 设置 JWT 的签发时间
.signWith(secretKey, Jwts.SIG.HS256) // 使用指定的算法和密钥进行签名
.expiration(expDate); // 设置 JWT 的过期时间
}
// 生成一个用于签名和验证的 SecretKey
public static SecretKey generalKey() {
byte[] encodeKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
return new SecretKeySpec(encodeKey, 0, encodeKey.length, "HmacSHA256");
}
// 解析并验证 JWT
public static Claims parseJWT(String jwt) throws Exception {
SecretKey secretKey = generalKey();
return Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(jwt)
.getPayload();
}
}JwtAuthenticationTokenFilter:JWT过滤器1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Resource
private UserMapper userMapper;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 从请求头Authorization中获取JWT
String token = request.getHeader("Authorization");
// 如果没有 JWT 或格式不正确,继续执行下一个过滤器
if (! StringUtils.hasText(token) || ! token.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
// 去掉Bearer 前缀
token = token.substring(7);
String userid;
try {
Claims claims = JwtUtil.parseJWT(token);
userid = claims.getSubject();
} catch (Exception e) {
// 可以继续细分异常
throw new RuntimeException(e);
}
User user = userMapper.getUserById(Integer.parseInt(userid));
if (user == null) {
throw new RuntimeException("用户不存在");
}
UserDetailsImpl loginUser = new UserDetailsImpl(user);
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser, null, null);
// 如果是有效的jwt,那么设置该用户为认证后的用户
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(request, response);
}
}在
SecurityConfig中添加配置1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36// 用于校验前端发来的JWT
@Resource
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
// 用于对密码进行编码及比对
// 在需要的地方可以自己手动注入使用
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// 开启授权保护
http.csrf(CsrfConfigurer::disable) // 基于token,不需要csrf
.sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 基于token,不需要session
.authorizeHttpRequests((authz) -> authz
// 不需要认证的地址
.requestMatchers(
"/login"
).permitAll() // 放行api
.requestMatchers(HttpMethod.OPTIONS)//跨域请求会先进行一次options请求
.permitAll()
.anyRequest() // 除上面外的所有请求全部需要鉴权认证
.authenticated()
)
// 添加JWT Filter
.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
授权
控制用户对应用程序资源的访问权限
在
UserDetailsImpl中添加角色/权限的集合通常角色命名为
ROLE_XXX,权限命名为角色名小写:操作,在getAuthorities中一起返回1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18private List<String> permissions;
private List<String> roles;
// 获取用户权限
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> authorities = new ArrayList<>();
if(roles != null) {
roles.forEach(role -> authorities.add(new SimpleGrantedAuthority(role)));
}
if (permissions != null) {
permissions.forEach(perm -> authorities.add(new SimpleGrantedAuthority(perm)));
}
return authorities;
}在
JwtAuthenticationTokenFilter时查询出用户的权限,封装到UserDetailsImpl中
鉴权
两种方法:
在配置类中添加
hasRole会自动在前面加上ROLE_前缀来匹配1
2
3
4
5
6// 只有 ADMIN 能访问
.requestMatchers("/admin/**").hasRole("ADMIN")
// ADMIN 和 USER 都能访问
.requestMatchers("/user/**").hasAnyRole("ADMIN", "USER")
// 要有 user:delete 权限才能访问
.requestMatchers("/user/delete/**").hasAuthority("user:delete")在方法上添加
1
2// 需要在配置类上加上@EnableMethodSecurity
@PreAuthorize("hasRole('ADMIN')")