SpringSecurity

Published 2026-08-03 10:03 Updated 2026-10-13 17:14 4910 words 25 min read ... Page views AI-reviewed An AI proof-read this post, but it still deserves a careful read. AI-rewritten Someone else wrote the original; AI helped me rewrite and polish it.

AI-reviewed An AI proof-read this post, but it still deserves a careful read.

This article mainly introduces the core concepts and implementation mechanisms of the Spring Security 6 enterprise-level security framework, focusing on the process of authentication and authorization, as well as the security processing architecture based on the filter chain. The content covers username and password authentication, JWT Token mode, CSRF and XSS protection, the working mechanism of SecurityFilterChain, the collaboration relationship between AuthenticationManager and AuthenticationProvider, and how to achieve stateless authentication through JWT in the front-end separation scenario, and emphasizes the importance of security configuration, cryptographic encoding specifications and context management in the production environment.

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 (Authentication): Confirm who the current visitor is and whether his identity is legal.
  • Access authorization: Verify whether an authenticated user has the right to access a resource or perform an operation.

Take the online education platform as an example:

user typean operation allowed to be performed
studentsview course
teachersPublish courses
administratordelete user

Certification solves "who you are" and authorization solves "what you can do".

Key capabilities of Spring Security

Common capabilities of Spring Security include:

  1. Identity authentication mechanisms such as username and password authentication, certificate authentication, and one-time token authentication.
  2. Access authorization at the URL level and method level.
  3. Password encoding and verification.
  4. Session management, fixed session attack protection and concurrent session control.
  5. CSRF protection, security response and other Web security capabilities.
  6. 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 itemSession stateful authenticationToken stateless authentication
state positionThe server saves the session, and the client usually saves the session CookieThe server usually does not save the login session, and the client carries an access token
cluster deploymentRequires session replication, shared storage, or sticky sessionsEach node can independently verify the token
Browser automatically carriesCookies are usually carried automatically by the browserAuthorization Request headers are usually actively added by client code
Cancellation and revocationThe server deletes the session and it will expire immediatelyRequires mechanisms such as short validity periods, blacklists, or token versions
typical scenarioServer-side rendering websites and homologous Web applicationsSeparate front and back APIs, microservices resource servers

Deposit Token localStorage This 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

image-001
image-001

Spring Security's Servlet security capabilities are built on a filter chain. Before the request enters the business controller, it will first go through Spring Security's selection. SecurityFilterChain。

Common core components are as follows:

assemblyrole
SecurityFilterChainDeclare the security filter that needs to be executed for the current request
FilterChainProxyMatch and execute the corresponding SecurityFilterChain
AuthenticationRepresents an authentication request or currently authenticated subject
AuthenticationManagerDefine authentication entry
AuthenticationProviderImplement a specific certification method
UserDetailsServiceLoad user information by user name
PasswordEncoderEncoding and verifying the password
SecurityContextSave the associated with the current request Authentication
SecurityContextHolderprovide access SecurityContext the unified entrance for
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";
    }
}

Access after starting the project http://localhost:8080/user/hello。When there is no custom security configuration, Spring Boot enables the default security configuration for the Web application and displays the login page.

image-002
image-002

default user

When there are no customizations in the project UserDetailsService、AuthenticationProvider When waiting for authentication configuration, Spring Boot usually creates default users:

username: user
password: 启动时随机生成

The random password is output to the console.

image-003
image-003

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 the PasswordEncoder Save the password summary.

Formation process of default login page

Under 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 Filter Above the mechanism. filter is located DispatcherServlet Before and controller, authentication, authorization, and security context management can be completed 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:

  1. Understand the overall request process of Spring Security.
  2. master SecurityFilterChain working mechanism.
  3. understood Authentication Two main uses.
  4. master AuthenticationManager with AuthenticationProvider relationship.
  5. understood SecurityContext with SecurityContextHolder responsibilities.
  6. 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, user request GET /api/user/delete/1 When, the security filter chain first determines whether the user has been authenticated and has delete permissions. 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.

image-004
image-004

DelegateFilterProxy and FilterChainProxy

Spring Security's Servlet request forwarding relationship can be summarized as:

DelegatingFilterProxy
→ FilterChainProxy
→ 匹配到的 SecurityFilterChain
→ 链内的 Security Filter
image-005
image-005
  1. DelegatingFilterProxy Registered in the Servlet container and responsible for delegating filtering work to the Spring container managed Filter Bean。
  2. FilterChainProxy It is the core entry of Spring Security and will match the appropriate entry based on the current request. SecurityFilterChain。
  3. the chosen SecurityFilterChain Implement the security filters in it in order.

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: Handle Bearer Tokens when configuring the OAuth 2.0 resource server.
  • AnonymousAuthenticationFilter: Provide anonymity for anonymous requests Authentication。
  • ExceptionTranslationFilter: Convert authentication and authorization exceptions into corresponding processing procedures.
  • AuthorizationFilter: Perform request-level authorization judgment.

This will appear in some older architecture diagrams FilterSecurityInterceptor。In common configurations of Spring Security 6, requests for authorization are usually made by AuthorizationFilter Done. The actual filter list should be based on the project startup log or debugging output.

Authentication model

image-006
image-006

Role of Authentication

Authentication There are two main uses:

  1. Submit as a certificate to be certified to AuthenticationManager。
  2. Represents the currently authenticated principal and saves it to the SecurityContext medium.

Common methods in the interface are as follows:

public interface Authentication extends Principal, Serializable {
    Object getPrincipal();

    Object getCredentials();

    Collection<? extends GrantedAuthority> getAuthorities();

    boolean isAuthenticated();
}
methodrole
getPrincipal()Returns the principal. After the user name and password authentication is successful, it is usually UserDetails
getCredentials()Returns the credential, usually a password; it may be cleared after successful authentication
getAuthorities()Returns a collection of roles, permissions, or scopes
isAuthenticated()Represents whether the current object has been authenticated

Status before and after certification

Created before logging in UsernamePasswordAuthenticationToken Usually contains only 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 Define unified certification entry:

Authentication authenticate(Authentication authentication)
        throws AuthenticationException;

ProviderManager is the most common AuthenticationManager Achieve. It will ask multiple questions in turn AuthenticationProvider, supported by current Authentication Type performs authentication.

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:

  • DaoAuthenticationProvider Handle database username and password authentication.
  • in the OAuth 2.0 resource server JwtAuthenticationProvider Process JWT Bearer Tokens.
  • custom AuthenticationProvider You can process business vouchers such as SMS Captcha.

UserDetailsService

Spring Security does not know where users are saving it, so it uses UserDetailsService Abstract user query process.

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 Save the current authentication object andSecurityContextHolder Provides access to the current security context.

Authentication authentication = SecurityContextHolder
        .getContext()
        .getAuthentication();

In Servlet applications,SecurityContextHolder used by default ThreadLocal Policy that allows code in the same request thread to read the current user.

ThreadLocal Just a default policy does not mean that a request always uses the same thread in all cases. 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. use ThreadLocal You can allow code in the same thread to directly obtain the 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
→ 执行认证成功处理

Subsequent use of customization for this course LoginController call AuthenticationManager。In this case, the login request will not pass through UsernamePasswordAuthenticationFilter, the controller is responsible for generating the JWT; subsequent requests are subjected to a custom JWT filter to parse the token and establish the SecurityContext。

this chapter summarizes

assemblyrole
SecurityFilterChainDefine security filters that requests need to be executed
FilterChainProxyMatch and execute the security filter chain
AuthenticationRepresents an authentication request or current authentication subject
AuthenticationManagerProvide unified certification entrance
ProviderManagerschedule multiple AuthenticationProvider
AuthenticationProviderImplement specific authentication logic
UserDetailsServiceLoad user information
PasswordEncoderCode and verify password
SecurityContextSave the current authentication object
SecurityContextHolderAccess the current security context
Interview Question: What is the difference between Authentication and UserDetails?

UserDetails Describe the account information loaded from the user data source, such as user name, password summary, account status, and permissions.

Authentication It can represent either the credential to be authenticated or the current entity that has passed the authentication. After successful certification, principal is usually a UserDetails Examples.

Front-end separation authentication

image-007
image-007

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 stable English to the role list role_key, used to generate ROLE_ADMIN、ROLE_USER Wait for permission identification.

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 checks it in subsequent requests Authorization The request header carries Bearer Token。

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 service

The HTTP status code should be handled separately from the service code in the service response body:

  • 401 Unauthorized Indicates that it is not authenticated, the Token is invalid, or the Token has expired.
  • 403 Forbidden Indicates that it has been authenticated but does not have 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 for this example only uses Authorization The request header is passed 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 Servlet Web security configuration for importing Spring Security. The real request rules are governed by SecurityFilterChain Bean definition;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

Can be reverse engineered using MyBatis Plus User、Role、Permission、Menu and its 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') automatically checks ROLE_ADMIN, so the stable role ID in the database should be saved as ADMIN, add it when constructing permissions ROLE_ Prefix.

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 Use an adaptive one-way hash algorithm with random salts. The same plaintext usually yields different results each time it is encoded, but it can all be encoded by matches() Complete verification.

@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 the matches(rawPassword, encodedPassword) Checks, 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. Available for actual projects ResponseEntity Or the global exception handler sets the HTTP status uniformly.

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 current request's SecurityContext。

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>
image-008
image-008

Access control actual combat

Activate method-level authorization

Use of Spring Security 6 @EnableMethodSecurity Activate 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 Method-level authorization will not be turned on by default, so you need to add it explicitly @EnableMethodSecurity。

use @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:

expressionmeaning
hasRole('ADMIN')whether it has ROLE_ADMIN
hasAnyRole('ADMIN', 'USER')Whether you have any specified roles
hasAuthority('permission:query')Whether you have the specified permissions
hasAnyAuthority('a', 'b')Whether you have any specified permissions
isAuthenticated()Whether the current user has been authenticated
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 a method,AuthorizationManagerBeforeMethodInterceptor read @PreAuthorize SpEL expression, combined with the current Authentication Conduct authorization judgment.

The target method will not be called until the verification passes; it will be thrown when the verification fails AccessDeniedException。In the HTTP request chain, this exception is usually converted to 403 Forbidden respond.

key considerations

  1. Successful authentication does not mean that you have all permissions. Authentication and authorization must be configured separately.
  2. 401 Indicates that it has not been certified,403 Indicates that it has been authenticated but has insufficient authority.
  3. JWT filters must first verify the signature and validity period before creating a certified Authentication。
  4. Don't trust roles or permissions submitted directly by clients.
  5. Do not return the password, password digest, and key in the Token or response body.
  6. CORS, CSRF, and XSS are different security issues and cannot be substituted for each other.
  7. The production system should use HTTPS and configure a reasonable Token validity period, refresh and revocation mechanism.
  8. 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.

Written by AI-rewritten

If you enjoyed this, leave a comment~

Loading...