Spring Security 6 Enterprise Security Framework
Spring Security Fundamentals
What is Spring Security
Spring Security is a security framework in the Spring ecosystem. It is mainly used to handle identity authentication, access authorization and common security protection in Servlet applications and responsive applications.
Compared with Apache Shiro, Spring Security integrates more closely with components such as Spring Boot, Spring MVC, and OAuth 2.0, and has richer extension points. Framework has many capabilities, so you should first master core concepts such as filter chains, authentication objects, and authorization rules when learning.
authentication and authorization
Enterprise applications often need to address two core issues:
- Authentication: Confirm who the current visitor is and whether his identity is legal.
- Authorization: Confirm whether an authenticated user has the right to access a resource or perform a certain operation.
Take the online education platform as an example:
| User Type | Allowed operations |
|---|---|
| Student | View Course |
| Teacher | releases course |
| Administrator | Delete user |
Certification solves "who you are" and authorization solves "what you can do".
Key capabilities of Spring Security
Common capabilities of Spring Security include:
- Identity authentication mechanisms such as username and password authentication, certificate authentication, and one-time token authentication.
- Access authorization at the URL level and method level.
- Password encoding and verification.
- Session management, fixed session attack protection and concurrent session control.
- CSRF protection, security response and other Web security capabilities.
- Integration of OAuth 2.0 clients, resource servers and OpenID Connect.
XSS mainly needs to be protected through output encoding, content security policies and front-end security development specifications. Spring Security can configure some security response headers, but it cannot replace business-level XSS protection.
CSRF
CSRF is cross-site request forgery. An attacker induces a user who has logged in to a legitimate site to visit a malicious page, and the browser may automatically carry the cookies of the legitimate site, making the legitimate site mistakenly believe that the request was initiated by the user himself.
XSS
XSS is a cross-site scripting attack. Attackers try to inject malicious scripts into web pages. When other users visit the page, the browser executes the script, which may cause information theft, identity fraud, or page tampering.
Session mode and Token mode
Spring Security supports both stateful and stateless authentication. The separation of front and back ends does not mean that only Tokens can be used, but in distributed APIs, stateless solutions using Bear Token are more common.
| Comparison Items | Session Stateful Certification | Token Stateful Certification |
|---|---|---|
| status location | The server saves the session, and the client usually saves the session Cookie | The server usually does not save the login session, and the client carries the access token |
| cluster deployment | requires session replication, shared storage, or sticky sessions | Each node can independently verify the token |
| browser automatically carries | cookies, usually the browser automatically carries | Authorization request header, usually the client code actively adds |
| Cancellation and revocation | The server deletes the session immediately. | requires mechanisms such as short validity period, blacklist or token version |
| typical scenarios | server-side rendering website, homologous Web application | front-end separation API, microservice resource server |
Depositing Tokens into localStorage will increase the risk of Tokens being stolen by XSS. Production systems should choose memory, protected cookies, or other storage options based on the threat model. If you use cookies automatically carried by your browser, you still need to evaluate and configure CSRF protection.
core architecture

Spring Security's Servlet security capabilities are built on a filter chain. Before the request enters the business controller, it will first go through the SecurityFilterChain selected by Spring Security.
Common core components are as follows:
| Component | Role |
|---|---|
SecurityFilterChain | declares the security filter that needs to be executed for the current request |
FilterChainProxy | Match and execute the corresponding SecurityFilterChain |
Authentication | represents a certification request or currently certified subject |
AuthenticationManager | Definition Certification Entrance |
AuthenticationProvider | Implement a specific certification method |
UserDetailsService | Load user information by user name |
PasswordEncoder | Code the password and verify the password |
SecurityContext | saves the Authentication |
SecurityContextHolder | provides unified access to SecurityContext |
interview question: What is the main role of Spring Security?
Spring Security mainly solves identity authentication and access authorization issues in applications, and provides security capabilities such as cryptographic encoding, session management, CSRF protection, security response headers, and OAuth 2.0 integration.
Spring Boot 3 integrates Spring Security
environmental requirements
This course example uses the following environment:
Spring Boot 3.3+
Java 17+
Spring Security 6
Maven
Vue 3
Introduce dependence
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>Write the first interface
@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping("/hello")
public String hello() {
return "hello security";
}
}Visit http://localhost:8080/user/hello after starting the project. When there is no custom security configuration, Spring Boot enables the default security configuration for the Web application and displays the login page.

default user
When there are no custom certification configurations such as UserDetailsService and AuthenticationProvider in the project, Spring Boot usually creates default users:
username: user
password: 启动时随机生成
The random password is output to the console.

Customize default username and password
spring:
security:
user:
name: admin
password: 123456
This configuration is suitable for entry demonstrations and not suitable for production environments. The production system should load users from a database, LDAP, or external identity service, and use PasswordEncoder to save a password summary.
Formation process of default login page
In the default configuration, the startup and access process can be summarized as:
Spring Boot 启动
→ 加载 Spring Security 自动配置
→ 创建安全过滤器链
→ 请求被过滤器链拦截
→ 发现请求尚未认证
→ 进入登录流程或返回认证响应
Interview Question: Why does Spring Security not just use Controllers to intercept requests?
Spring Security's Servlet support is built on top of the Filter mechanism. The filter is located in front of DispatcherServlet and the controller and can complete authentication, authorization and security context management before requests enter the business layer.
A typical sequence can be simplified to:
Servlet Filter → DispatcherServlet → HandlerInterceptor → Controller
Core principles of Spring Security
learning goals
After completing this chapter, you should be able to:
- Understand the overall request process of Spring Security.
- Master the working mechanism of
SecurityFilterChain. - Understand the two main uses of
Authentication. - Understand the relationship between
AuthenticationManagerandAuthenticationProvider. - Understand the responsibilities of
SecurityContextandSecurityContextHolder. - Analyze the main execution steps of username and password authentication.
Filter-based security processing
In traditional Spring MVC applications, the request process can be simplified to:
客户端请求
→ Servlet Filter
→ DispatcherServlet
→ HandlerInterceptor
→ Controller
→ Service
→ DAO
→ Database
Spring Security selects filters as security entrances to servlets so that authentication and authorization can be completed before business code is executed.
For example, when a user requests GET /api/user/delete/1, the security filter chain first determines whether the user has been authenticated and has delete rights. When verification fails, the request does not enter the target controller.
SecurityFilterChain
SecurityFilterChain represents a set of filters used to handle security tasks. An application can declare multiple chains of security filters, each chain responsible for a different request scope.

DelegateFilterProxy and FilterChainProxy
Spring Security's Servlet request forwarding relationship can be summarized as:
DelegatingFilterProxy
→ FilterChainProxy
→ 匹配到的 SecurityFilterChain
→ 链内的 Security Filter

DelegatingFilterProxyis registered in the Servlet container and is responsible for delegating filtering work to theFilterBean managed by the Spring container.FilterChainProxyis the core entry of Spring Security and will match the appropriateSecurityFilterChainbased on the current request.- The selected
SecurityFilterChainexecutes the safety filters in sequence.
The specific composition of the filter depends on the configuration. Common filters include:
SecurityContextHolderFilter: Load and manage security contexts.CsrfFilter: Verify CSRF Token when enabling CSRF.UsernamePasswordAuthenticationFilter: Enable processing of user name and password login requests when logging in form.BearerTokenAuthenticationFilter: Processing Bear Token when configuring the OAuth 2.0 resource server.AnonymousAuthenticationFilter: Provide anonymousAuthenticationfor anonymous requests.ExceptionTranslationFilter: Convert authentication and authorization exceptions into corresponding processing procedures.AuthorizationFilter: Perform request-level authorization judgment.
FilterSecurityInterceptor will appear in some old architecture diagrams. In common configurations of Spring Security 6, requests for authorization are usually completed by AuthorizationFilter. The actual filter list should be based on the project startup log or debugging output.
Authentication model

Role of Authentication
Authentication has two main uses:
- Submit it to
AuthenticationManageras a certificate to be certified. - Represents the entity that has been currently certified and saves it in
SecurityContext.
Common methods in the interface are as follows:
public interface Authentication extends Principal, Serializable {
Object getPrincipal();
Object getCredentials();
Collection<? extends GrantedAuthority> getAuthorities();
boolean isAuthenticated();
}| Method | Action |
|---|---|
getPrincipal() | Returns to the main body. After successful user name and password authentication, it is usually UserDetails |
getCredentials() | Returns the certificate, usually a password; it may be cleared after successful authentication |
getAuthorities() | Returns the set of roles, permissions, or scopes |
isAuthenticated() | indicates whether the current object has been certified |
Status before and after certification
UsernamePasswordAuthenticationToken created before logging in usually only contains a username and password and is in an unauthenticated state:
UsernamePasswordAuthenticationToken
├── principal: admin
├── credentials: 123456
├── authorities: []
└── authenticated: false
The new object returned after successful authentication usually contains user details and permissions, and is in an authenticated state:
UsernamePasswordAuthenticationToken
├── principal: UserDetails
├── credentials: null
├── authorities: [ROLE_ADMIN, permission:query]
└── authenticated: true
Do not directly mark any unverified object as authenticated in an unsafe manner.
AuthenticationManager and AuthenticationProvider
AuthenticationManager defines unified certification entry:
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
ProviderManager is the most common implementation of AuthenticationManager. It will query multiple AuthenticationProvider in turn, and the Provider that supports the current Authentication type performs certification.
A typical username and password authentication process is as follows:
Authentication
→ AuthenticationManager
→ ProviderManager
→ DaoAuthenticationProvider
→ UserDetailsService
→ PasswordEncoder
Different authentication methods can use different providers. For example:
DaoAuthenticationProviderhandles database username and password authentication.JwtAuthenticationProviderin the OAuth 2.0 resource server handles JWT Bear Tokens.- Custom
AuthenticationProvidercan process business vouchers such as SMS Captcha.
UserDetailsService
Spring Security does not know where users are saving it, so it abstracts the user query process through UserDetailsService.
public interface UserDetailsService {
UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException;
}
User information can come from MySQL, Redis, LDAP, or other user centers.
Here is a simplified example:
@Service
@RequiredArgsConstructor
public class UserService implements UserDetailsService {
private final UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) {
User user = userMapper.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("账号不存在");
}
return org.springframework.security.core.userdetails.User
.withUsername(user.getUsername())
.password(user.getPassword())
.roles("USER")
.build();
}
}SecurityContext and SecurityContextHolder
SecurityContext saves the current authentication object, and SecurityContextHolder provides access to the current security context.
Authentication authentication = SecurityContextHolder
.getContext()
.getAuthentication();
In Servlet applications, SecurityContextHolder uses the ThreadLocal policy by default, so that code in the same request thread can read the current user.
ThreadLocal is just a default policy and does not mean that a request always uses the same thread in all situations. Asynchronous tasks, thread pools, and responsive programming require the use of appropriate context propagation mechanisms.
interview question: Why does SecurityContext use ThreadLocal by default?
Default Servlet requests are usually processed in threads. Using ThreadLocal allows code in the same thread to directly obtain current authentication information, avoid passing parameters layer by layer, and isolate the security contexts of different requesting threads. Spring Security cleans the context after the request ends to avoid identity disclosure caused by thread reuse.
User name password login process
When enabling form login, a typical process is as follows:
POST /login
→ UsernamePasswordAuthenticationFilter
→ 创建未认证 UsernamePasswordAuthenticationToken
→ AuthenticationManager
→ ProviderManager
→ DaoAuthenticationProvider
→ UserDetailsService 查询用户
→ PasswordEncoder 校验密码
→ 返回已认证 Authentication
→ 保存到 SecurityContext
→ 执行认证成功处理Later in this course, custom LoginController is used to call AuthenticationManager. In this case, the login request will not go through UsernamePasswordAuthenticationFilter, and the controller is responsible for generating JWT; subsequent requests will be parsed by a custom JWT filter and the SecurityContext of the current request will be established.
this chapter summarizes
| Component | Role |
|---|---|
SecurityFilterChain | defines the security filter that requests need to be executed |
FilterChainProxy | Matches and implements safety filter chain |
Authentication | represents a certification request or current certification subject |
AuthenticationManager | provides unified certification entrance |
ProviderManager | Dispatch multiple AuthenticationProvider |
AuthenticationProvider | implements specific certification logic |
UserDetailsService | Load user information |
PasswordEncoder | Code and verify password |
SecurityContext | Save current certification object |
SecurityContextHolder | Access the current security context |
Interview Question: What is the difference between Authentication and UserDetails?
UserDetails describes account information loaded from user data sources, such as user name, password summary, account status and permissions.
Authentication can represent either the certificate to be authenticated or the current entity that has passed the authentication. After successful certification, its principal is usually an example of UserDetails.
Front-end separation authentication

This chapter adopts the following scheme:
用户名密码登录
→ AuthenticationManager 校验
→ 服务端签发 JWT
→ 客户端使用 Authorization 请求头携带 JWT
→ JWT 过滤器验签并建立 SecurityContext
→ Spring Security 执行访问授权
database table structure
The following example maintains user, role, permissions, and menu relationships. Add a stable English role_key to the role table, which is used to generate ROLE_ADMIN, ROLE_USER and other permission identifiers.
DROP TABLE IF EXISTS `t_menu_role`;
DROP TABLE IF EXISTS `t_role_permission`;
DROP TABLE IF EXISTS `t_menu`;
DROP TABLE IF EXISTS `t_permission`;
DROP TABLE IF EXISTS `t_user`;
DROP TABLE IF EXISTS `t_role`;
CREATE TABLE `t_menu` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` VARCHAR(255) DEFAULT NULL COMMENT '菜单名称',
`icon` VARCHAR(255) DEFAULT NULL COMMENT '菜单图标',
`url` VARCHAR(255) DEFAULT NULL COMMENT '菜单 URL',
`pid` BIGINT DEFAULT NULL COMMENT '父菜单 ID',
`remark` VARCHAR(255) DEFAULT NULL COMMENT '备注',
`level` INT DEFAULT NULL COMMENT '菜单层级',
`is_link` TINYINT DEFAULT 0 COMMENT '是否可跳转',
PRIMARY KEY (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
CREATE TABLE `t_role` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`role_name` VARCHAR(255) NOT NULL,
`role_key` VARCHAR(64) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_role_key` (`role_key`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
CREATE TABLE `t_permission` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`permission_name` VARCHAR(255) NOT NULL,
`permission_key` VARCHAR(255) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_permission_key` (`permission_key`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
CREATE TABLE `t_user` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`username` VARCHAR(255) NOT NULL,
`password` VARCHAR(255) NOT NULL,
`role_id` BIGINT NOT NULL,
`nickname` VARCHAR(255) DEFAULT NULL,
`email` VARCHAR(255) DEFAULT NULL,
`phone` VARCHAR(255) DEFAULT NULL,
`freeze` TINYINT DEFAULT 0,
`dept_id` BIGINT DEFAULT NULL,
`remark` VARCHAR(255) DEFAULT NULL,
`insert_time` DATETIME DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
CREATE TABLE `t_role_permission` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`role_id` BIGINT NOT NULL,
`permission_id` BIGINT NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_role_permission` (`role_id`, `permission_id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
CREATE TABLE `t_menu_role` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`menu_id` BIGINT NOT NULL,
`role_id` BIGINT NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_menu_role` (`menu_id`, `role_id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
START TRANSACTION;
INSERT INTO `t_role` (`id`, `role_name`, `role_key`) VALUES
(1, '系统管理员', 'ADMIN'),
(2, '普通用户', 'USER');
INSERT INTO `t_permission` (`id`, `permission_name`, `permission_key`) VALUES
(1, '查询', 'permission:query'),
(2, '新增', 'permission:insert'),
(3, '修改', 'permission:update'),
(4, '删除', 'permission:delete');
INSERT INTO `t_role_permission` (`id`, `role_id`, `permission_id`) VALUES
(50, 1, 1),
(51, 1, 2),
(52, 1, 3),
(53, 1, 4),
(61, 2, 1);
INSERT INTO `t_menu` (`id`, `name`, `icon`, `url`, `pid`, `remark`, `level`, `is_link`) VALUES
(1, '系统设置', 'layui-icon-fire', NULL, -1, '', 1, 0),
(2, '分类管理', 'layui-icon-name', 'category/list', 1, '', 2, 1),
(3, '商品管理', 'layui-icon-service', NULL, -1, '', 1, 0),
(4, '商品列表', 'layui-icon-rate', 'asset-info/list', 3, '', 2, 1);
INSERT INTO `t_menu_role` (`id`, `menu_id`, `role_id`) VALUES
(1, 1, 1),
(2, 2, 1),
(3, 3, 1),
(4, 4, 1),
(5, 3, 2),
(6, 4, 2);
INSERT INTO `t_user`
(`id`, `username`, `password`, `role_id`, `nickname`, `insert_time`)
VALUES
(1, 'admin',
'$2a$10$t9BSP6hInmZm5RJocXVMdOLXzVXh4wgiBaYrM6iUslsrb.5z.eYce',
1, '超级管理员', '2021-05-26 14:41:06'),
(2, 'normal',
'$2a$10$Mv1ruD0gHy9Uq73SbfH80ep1McuJNZiJCjM3BpxAJVr9pt34iwWlS',
2, '普通用户', NULL);
COMMIT;Back-end dependence
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.6</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>JJWT modules in the same project should use the same version.
Front-end request interceptor
After successful login, the front end saves the access token and carries Bearer Token in the Authorization request header of subsequent requests.
import axios from 'axios'
const service = axios.create({
baseURL: 'http://localhost:8080',
timeout: 5000
})
service.interceptors.request.use(
config => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
error => Promise.reject(error)
)
service.interceptors.response.use(
response => response.data,
error => {
const status = error.response?.status
if (status === 401) {
localStorage.removeItem('token')
window.location.href = '/login'
}
return Promise.reject(error)
}
)
export default serviceThe HTTP status code should be handled separately from the service code in the service response body:
401 Unauthorizedmeans that it is not authenticated, the Token is invalid, or the Token has expired.403 Forbiddensaid it has been certified but has no access rights.
Security core configuration
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
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.configurers.AbstractHttpConfigurer;
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;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.nio.charset.StandardCharsets;
import java.util.List;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private static final String[] WHITE_LIST = {
"/login",
"/doc.html",
"/webjars/**",
"/favicon.ico",
"/druid/**",
"/public/**"
};
@Bean
public SecurityFilterChain securityFilterChain(
HttpSecurity http,
JwtAuthenticationFilter jwtAuthenticationFilter) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.cors(Customizer.withDefaults())
.formLogin(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.exceptionHandling(exceptions -> exceptions
.authenticationEntryPoint((request, response, exception) -> {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.getWriter().write(
"{\"code\":401,\"msg\":\"未登录或令牌无效\"}"
);
})
.accessDeniedHandler((request, response, exception) -> {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.getWriter().write(
"{\"code\":403,\"msg\":\"没有访问权限\"}"
);
}))
.authorizeHttpRequests(authorize -> authorize
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.requestMatchers(WHITE_LIST).permitAll()
.anyRequest().authenticated())
.addFilterBefore(
jwtAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class
);
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(List.of("http://localhost:5173"));
configuration.setAllowedMethods(List.of(
"GET", "POST", "PUT", "DELETE", "OPTIONS"
));
configuration.setAllowedHeaders(List.of(
HttpHeaders.AUTHORIZATION,
HttpHeaders.CONTENT_TYPE
));
configuration.setExposedHeaders(List.of(HttpHeaders.AUTHORIZATION));
configuration.setAllowCredentials(false);
UrlBasedCorsConfigurationSource source =
new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}
}The Token in this example is only passed through the Authorization request header, and the server does not rely on browser cookies, so CSRF is turned off. If you change to cookies to automatically carry credentials, you should re-evaluate and enable appropriate CSRF protection.
@EnableWebSecurity is used to import Spring Security's Servlet Web Security configuration. The real request rules are defined by the SecurityFilterChain Bean;Spring Boot Starter is responsible for related automatic configuration and basic integration.
CORS allowed sources in the production environment must be configured as real front-end domain names, and no source should be unconditionally released.
Generating Entity and Mapper
You can use MyBatis Plus to reverse engineer User, Role, Permission, Menu and their Mapper. After generation, field types, table association queries and null value processing still need to be checked, and the reverse engineering result cannot be directly regarded as a complete business code.
Implement UserDetails
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
import java.util.stream.Stream;
public class UserItem implements UserDetails {
private final User user;
private final List<String> permissions;
private final List<GrantedAuthority> authorities;
public UserItem(User user, String roleKey, List<String> permissions) {
this.user = user;
this.permissions = List.copyOf(permissions);
this.authorities = Stream.concat(
Stream.of(new SimpleGrantedAuthority("ROLE_" + roleKey)),
permissions.stream().map(SimpleGrantedAuthority::new)
)
.map(GrantedAuthority.class::cast)
.toList();
}
public User getUser() {
return user;
}
public List<String> getPermissions() {
return permissions;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return user.getFreeze() == null || user.getFreeze() == 0;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}hasRole('ADMIN') will automatically check ROLE_ADMIN, so the stable role ID in the database should be saved as ADMIN, and the ROLE_ prefix should be added when constructing permissions.
Implement UserDetailsService
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.RequiredArgsConstructor;
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.List;
@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserMapper userMapper;
private final RoleMapper roleMapper;
private final PermissionMapper permissionMapper;
@Override
public UserDetails loadUserByUsername(String username) {
User user = userMapper.selectOne(
new LambdaQueryWrapper<User>()
.eq(User::getUsername, username)
.last("LIMIT 1")
);
if (user == null) {
throw new UsernameNotFoundException("账号不存在");
}
Role role = roleMapper.selectById(user.getRoleId());
if (role == null) {
throw new UsernameNotFoundException("账号未配置有效角色");
}
List<String> permissions = permissionMapper
.selectPermissionListByRoleId(user.getRoleId())
.stream()
.map(Permission::getPermissionKey)
.toList();
return new UserItem(user, role.getRoleKey(), permissions);
}
}BCrypt cryptographic encoding
BCryptPasswordEncoder uses an adaptive one-way hash algorithm with random salts. The same plaintext is usually encoded with different results each time, but it can be verified through matches().
@Test
void shouldEncodeAndMatchPassword() {
PasswordEncoder encoder = new BCryptPasswordEncoder();
String encodedPassword = encoder.encode("123456");
boolean matched = encoder.matches("123456", encodedPassword);
System.out.println(encodedPassword);
System.out.println(matched);
}Password digest cannot be "decrypted" back into plaintext. Use matches(rawPassword, encodedPassword) verification when logging in, rather than comparing strings after encoding again.
Unified response object
public record Result<T>(int code, T data, String msg, long count) {
public static <T> Result<T> ok(T data, String msg) {
return new Result<>(200, data, msg, 0);
}
public static Result<Void> ok(String msg) {
return new Result<>(200, null, msg, 0);
}
public static Result<Void> fail(int code, String msg) {
return new Result<>(code, null, msg, 0);
}
public static Result<Void> fail(String msg) {
return fail(500, msg);
}
}The business response code should maintain clear semantics with the HTTP status code. Actual projects can use ResponseEntity or global exception handler to uniformly set HTTP status.
JWT Certification Filter
The custom login interface is only responsible for issuing tokens. Each subsequent protected request also needs to resolve the Token and put the authentication result into the SecurityContext of the current request.
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.List;
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private static final String BEARER_PREFIX = "Bearer ";
private final JwtUtil jwtUtil;
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String authorization = request.getHeader(HttpHeaders.AUTHORIZATION);
if (authorization == null || !authorization.startsWith(BEARER_PREFIX)) {
filterChain.doFilter(request, response);
return;
}
String token = authorization.substring(BEARER_PREFIX.length()).trim();
try {
Claims claims = jwtUtil.parseToken(token);
if (SecurityContextHolder.getContext().getAuthentication() == null) {
List<?> values = claims.get("permissions", List.class);
List<GrantedAuthority> authorities = values == null
? List.of()
: values.stream()
.filter(String.class::isInstance)
.map(String.class::cast)
.map(SimpleGrantedAuthority::new)
.map(GrantedAuthority.class::cast)
.toList();
UsernamePasswordAuthenticationToken authentication =
UsernamePasswordAuthenticationToken.authenticated(
claims.getSubject(),
null,
authorities
);
SecurityContext context =
SecurityContextHolder.createEmptyContext();
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
}
filterChain.doFilter(request, response);
} catch (JwtException | IllegalArgumentException exception) {
SecurityContextHolder.clearContext();
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(
"{\"code\":401,\"msg\":\"令牌无效或已过期\"}"
);
}
}
}This example uses permissions in JWT directly. The advantage is that there is no need to query the database every time. The disadvantage is that permission changes will not immediately affect the old Token. The production system can use short-term tokens, token version numbers, blacklists, or re-query key permissions for each request.
Customize login interface
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
@RequiredArgsConstructor
public class LoginController {
private final AuthenticationManager authenticationManager;
private final MenuService menuService;
private final JwtUtil jwtUtil;
@PostMapping("/login")
public Result<Map<String, Object>> login(
@RequestBody LoginRequest request) {
Authentication authentication = authenticationManager.authenticate(
UsernamePasswordAuthenticationToken.unauthenticated(
request.username(),
request.password()
)
);
UserItem userItem = (UserItem) authentication.getPrincipal();
User user = userItem.getUser();
List<String> authorities = authentication.getAuthorities()
.stream()
.map(GrantedAuthority::getAuthority)
.toList();
String token = jwtUtil.createToken(
user.getId(),
user.getUsername(),
user.getRoleId(),
authorities
);
List<Menu> menuList = menuService.listByRoleId(user.getRoleId());
Map<String, Object> data = new HashMap<>();
data.put("token", token);
data.put("user", user);
data.put("menuList", menuList);
data.put("authorities", authorities);
return Result.ok(data, "登录成功");
}
public record LoginRequest(String username, String password) {
}
}It is recommended to hand over authentication exceptions to the global exception handler for unified conversion:
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.LockedException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BadCredentialsException.class)
public ResponseEntity<Result<Void>> handleBadCredentials() {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(Result.fail(401, "用户名或密码错误"));
}
@ExceptionHandler(LockedException.class)
public ResponseEntity<Result<Void>> handleLocked() {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(Result.fail(403, "账号已被冻结"));
}
}Do not put sensitive fields such as passwords and password summaries directly into the login response. You can create a dedicated user response object that returns only the fields actually needed by the front end.
interface test
After successful login, the Token, basic user information, menu and permission set are returned in the response. Subsequent requests should carry:
Authorization: Bearer <token>

Access control actual combat
Activate method-level authorization
Spring Security 6 uses @EnableMethodSecurity to enable method-level authorization.
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
@SpringBootApplication
@EnableMethodSecurity
public class SecurityApplication {
public static void main(String[] args) {
SpringApplication.run(SecurityApplication.class, args);
}
}spring-boot-starter-security will not enable method-level authorization by default, so @EnableMethodSecurity needs to be explicitly added.
Using @PreAuthorize
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class PermissionController {
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/admin")
public String admin() {
return "管理员接口访问成功";
}
@PreAuthorize("hasAuthority('permission:query')")
@GetMapping("/user/list")
public String userList() {
return "用户列表接口访问成功";
}
@PreAuthorize("hasAnyRole('ADMIN', 'USER')")
@GetMapping("/common")
public String common() {
return "通用接口访问成功";
}
}Common expressions are as follows:
| Expression | Meaning |
|---|---|
| Does | hasRole('ADMIN') |
| Does | hasAnyRole('ADMIN', 'USER') |
hasAuthority('permission:query') Does | have the specified authority |
| Does | hasAnyAuthority('a', 'b') |
isAuthenticated() | Have current users certified |
interview question: What is the execution principle of @PreAuthorize?
With method security enabled, Spring Security creates proxies for protected Spring beans based on Spring AOP. Before calling the method, AuthorizationManagerBeforeMethodInterceptor reads the SpEL expression of @PreAuthorize and makes authorization judgment based on the current Authentication.
The target method will not be called until the verification passes; AccessDeniedException will be thrown when the verification fails. In the HTTP request chain, this exception is usually converted into a 403 Forbidden response.
key considerations
- Successful authentication does not mean that you have all permissions. Authentication and authorization must be configured separately.
401indicates that it has not been certified, and403indicates that it has been certified but lacks authority.- The JWT filter must first verify the signature and expiration date before creating a certified
Authentication. - Don't trust roles or permissions submitted directly by clients.
- Do not return the password, password digest, and key in the Token or response body.
- CORS, CSRF, and XSS are different security issues and cannot be substituted for each other.
- The production system should use HTTPS and configure a reasonable Token validity period, refresh and revocation mechanism.
- Method-level authorization is suitable for protecting key operations at the Service layer and cannot just rely on whether the front-end button is displayed.
If you enjoyed this, leave a comment~