Spring-Security笔记

本文最后更新于:2025年2月18日 下午

Spring-Security

一、配置类方法

1、Maven依赖

1
2
3
4
5
6
7
8
9
10
11
12
<!--  springboot整合security的依赖[必选]  -->
<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>

2、配置类

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
package com.example.config;

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {

// 此处需自定义类实现此接口后才能注入
private final UserDetailsService userDetailsService;

//
@Override
protected void configure(HttpSecurity http) throws Exception {

// 安全策略 >> 跨域资源访问和跨站请求伪造 >> 禁用 [可选]
http.cors().and().csrf().disable();

// 自定义无权限访问的403页面
http.exceptionHandling().accessDeniedPage("/403");

// 无需认证即可访问的路径
http.authorizeRequests()
.antMatchers("/login").permitAll()
.anyRequest().authenticated();

http.formLogin() // 开启表单登录
.loginPage("/login") // 自定义的登录页路径
.loginProcessingUrl("/user/login") // 前端表单的提交路径[action],且提交方式必须为[POST]
.defaultSuccessUrl("/")// 成功后默认跳转的路径:例如首页
.failureUrl("/logout")// 失败后跳转的路径:例如返回登录页
.usernameParameter("username") // 用户名参数,若参数名非username,则需要配置此项
.passwordParameter("password") // 密码参数,若参数名非password,则需要配置此项
.successHandler() // 成功的后续处理方法 >> 需要自定义方法并实现接口AuthenticationSuccessHandler() 例如生成jwt令牌等
.failureHandler(); // 成功的后续处理方法 >> 需要自定义方法并实现接口 例如返回错误信息
http.logout().logoutUrl("/logout"). //自定义退出登录
logoutSuccessUrl("/login");
}

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {

//这里要设置自定义认证 >>> 自定义认证策略
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}


/**
* 配置加密类
* @author peter
* @date 2024/10/7
* @className SecurityConfig
* @packageName com.example.config
* @description TODO
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}

3、自定义认证类

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
package com.example.service;

import com.example.domain.User;
import com.example.mapper.UserMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

/**
* 自定义认证类 >> 可通过查询数据库来认证登录用户
* @author peter
* @date 2024/10/7
* @description TODO
*/
@Service
@RequiredArgsConstructor
public class userDetailsServiceImpl implements UserDetailsService {

// 构造器注入的Mapper类
private final UserMapper userMapper;

// 登录的用户名需要与这里的相同 >> 这里为了测试,简化了操作
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 用户角色不能为空
List<GrantedAuthority> grantedAuthorities = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
User user = new User();
user.setUsername(username);
user.setPassword(new BCryptPasswordEncoder().encode("123456"));// 加密密码


return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), grantedAuthorities);
}
}

4、用户实体类

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
91
92
93
94
95
96
97
package com.example.domain;

import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.Collections;
import java.util.Set;
import java.util.stream.Collectors;

@Data
@TableName("user")
public class User implements UserDetails {
@TableId
private Long id;
private String username;
private String password;


private String role; // 单角色
private Set<String> roles; // 多角色版



/**
* 获取用户角色
* @author peter
* @date 2024/10/7
* @className User
* @packageName com.example.domain
* @return 返回用户的所有角色
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {

// 单角色版
//return AuthorityUtils.commaSeparatedStringToAuthorityList(this.role);

// 多角色版
return roles.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}

/**
* 判断账户是否到期
* @author peter
* @date 2024/10/7
* @className User
* @packageName com.example.domain
*/
@Override
public boolean isAccountNonExpired() {
return true;
}

/**
* 判断账户是否未锁定
* @author peter
* @date 2024/10/7
* @className User
* @packageName com.example.domain
*/
@Override
public boolean isAccountNonLocked() {
return true;
}

/**
* 判断凭证是否到期
* @author peter
* @date 2024/10/7
* @className User
* @packageName com.example.domain
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}

/**
* 判断是否可用
* @author peter
* @date 2024/10/7
* @className User
* @packageName com.example.domain
*/
@Override
public boolean isEnabled() {
return true;
}
}

5、自定义登录后续处理器

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
package com.example.service;

import com.example.utils.JwtTokenUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class CustomAuthenticationHandler implements AuthenticationSuccessHandler, AuthenticationFailureHandler {

@Autowired
private JwtTokenUtil jwtTokenUtil;

@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException {
// 这里可以添加登录成功后的处理逻辑
// 例如返回 JWT token
String username = authentication.getName(); // 获取用户名

// 生成 JWT
String token = jwtTokenUtil.generateToken(username);

// 设置返回的内容类型为 JSON
response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_OK);

// 返回 JWT token
response.getWriter().write("{\"token\": \"" + token + "\"}");
response.getWriter().flush();
}

@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
//TODO: 登录失败后的处理逻辑 >> 例如统计失败次数等等
}
}

6、配置文件

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
server:
port: 80

spring:
# 数据库连接池
datasource:
# 数据库驱动名称
driver-class-name: com.mysql.cj.jdbc.Driver
# 用户名
username: root
# 密码
password: 123456
# 地址
url: jdbc:mysql://127.0.0.1:3306/demo?characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowMultiQueries=true


thymeleaf:
enabled: true

# mybatis-plus配置
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 类别名指定包
type-aliases-package: com.example.entity

# 全局配置
global-config:
# 数据库配置
db-config:
# 逻辑未删除的值
logic-not-delete-value: 0
# 逻辑删除的值
logic-delete-value: 1
# 逻辑删除指定属性字段
logic-delete-field: deleted



jwt: # jwt配置
secret: key # 加密密文
expiration: 3600000 # 1 hour in milliseconds

7、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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package com.example.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

@Component
public class JwtTokenUtil {

@Value("${jwt.secret}")
private String secret;

@Value("${jwt.expiration}")
private long expiration;

public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}

public Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}

public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}

private Claims extractAllClaims(String token) {
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
}

private Boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}

public String generateToken(String userDetails) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, userDetails);
}

private String createToken(Map<String, Object> claims, String subject) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(SignatureAlgorithm.HS256, secret)
.compact();
}

public Boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
}

二、注解方法

要使用注解,需先开启注解使用,可在启动类或配置类上使用以下注解开启

1
@EnableGlobalMethodSecurity(securedEnabled = true)

完整启动类示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.example;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;

@MapperScan("com.example.mapper")
@EnableGlobalMethodSecurity(securedEnabled = true)
@SpringBootApplication
public class Main {
public static void main(String[] args) {
SpringApplication.run(Main.class, args);
}
}

1、@Secured

被此注解修饰下的方法,访问者必须有注解中所指示的角色,否则不允许访问。

角色前要加ROLE_

示例:

1
2
3
4
5
6
@GetMapping("/")
@ResponseBody
@Secured({"ROLE_ADMIN","ROLE_USER"}) // 这里说明只允许角色ADMIN和USER访问
public String index() {
return "index";
}

2、@PreAuthorize

此注解修饰下的方法,会在进入方法前验证身份

需要先在开启注解的参数里添加:prePostEnabled = true

启动类完整示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.example;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;

@MapperScan("com.example.mapper")
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
@SpringBootApplication
public class Main {
public static void main(String[] args) {
SpringApplication.run(Main.class, args);
}
}

方法上完整示例

1
2
3
4
5
6
@GetMapping("/")
@ResponseBody
@PreAuthorize("hasAnyAuthority('ADMIN', 'USER')") // 这里说明只允许权限ADMIN和USER访问
public String index() {
return "index";
}

3、@PostAuthorize

此注解修饰下的方法,会在方法执行后验证身份 >>> 一般用于验证返回值

需要先在开启注解的参数里添加:prePostEnabled = true

完整示例:

1
2
3
4
5
6
@GetMapping("/")
@ResponseBody
@PostAuthorize("hasAnyAuthority('ADMIN', 'USER')") // 这里说明只允许权限ADMIN和USER访问
public String index() {
return "index";
}

4、@PreFilter

此注解修饰下的方法,会过滤传入到方法的数据

完整示例:

1
2
3
4
5
6
7
@GetMapping("/")
@ResponseBody
@PostAuthorize("hasAnyAuthority('ADMIN', 'USER')") // 这里说明只允许权限ADMIN和USER访问
@PreFilter("filterObject.contains('admin')") //传入值中有admin就执行,否则不执行
public String index() {
return "index";
}

5、@PostFilter

此注解修饰下的方法,会过滤返回的数据

完整示例:filterObject标识任意对象

1
2
3
4
5
6
7
8
9
10
11
@GetMapping("/")
@ResponseBody
@PostAuthorize("hasAnyAuthority('admin', 'user')")
@PostFilter("filterObject.contains('admin')") //返回值中有admin就返回,否则返回空值
public List<String> index() {
System.out.println(">>>>>>>");
List<String> list = new ArrayList<>();
//list.add("admin");
list.add("user");
return list;
}

Spring-Security笔记
https://superlovelace.top/2024/10/18/SpringSecurity笔记/
作者
棱境
发布于
2024年10月18日
更新于
2025年2月18日
许可协议