Master Spring Security: From Quick Start to Advanced JWT Authentication and RBAC

This comprehensive guide walks you through Spring Security fundamentals, setting up a Spring Boot project, configuring authentication with JWT and Redis, implementing RBAC permission management, customizing error handling, enabling CORS, and addressing CSRF, providing complete code examples and detailed explanations for secure backend development.

Java High-Performance Architecture
Java High-Performance Architecture
Java High-Performance Architecture
Master Spring Security: From Quick Start to Advanced JWT Authentication and RBAC

Introduction

Overview

Spring Security is a security management framework in the Spring family. Compared with Shiro, it provides richer features and a larger community. Large‑scale projects usually use Spring Security, while smaller projects may prefer Shiro for its simplicity.

Web applications typically require authentication (verifying the user) and authorization (checking permissions).

Authentication: Verify that the current visitor is a legitimate user and identify which user it is.

Authorization: After authentication, determine whether the user has permission to perform a specific operation.

Authentication and authorization are the core functions of Spring Security.

1. Quick Start

1.1 Preparation

First, create a simple Spring Boot project.

① Set parent project and add dependencies

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.5.0</version>
</parent>
<dependencies>
   <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
   </dependency>
   <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
   </dependency>
</dependencies>

② Create the main class

@SpringBootApplication
public class SecurityApplication {
    public static void main(String[] args) {
        SpringApplication.run(SecurityApplication.class, args);
    }
}

③ Create a controller

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {
    @RequestMapping("/hello")
    public String hello() {
        return "hello";
    }
}

1.2 Add Spring Security

In a Spring Boot project, simply add the Spring Security starter dependency.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

After adding the dependency, accessing the previous endpoint will automatically redirect to Spring Security’s default login page (username: user, password printed in the console). You must log in before accessing the API.

2. Authentication

2.1 Login Verification Process

2.2 Principle Overview

2.2.1 Complete Spring Security Flow

Spring Security works as a filter chain that contains various functional filters.

The diagram shows only core filters; non‑core filters are omitted.

UsernamePasswordAuthenticationFilter : Handles login requests after the user submits username and password.

ExceptionTranslationFilter : Handles any AccessDeniedException or AuthenticationException thrown in the chain.

FilterSecurityInterceptor : Performs permission checks.

You can view the current filter chain order via debugging.

@SpringBootApplication
public class SecurityApplication {
    public static void main(String[] args) {
        ConfigurableApplicationContext run = SpringApplication.run(SecurityApplication.class, args);
        System.out.println("----");
    }
}

2.2.2 Detailed Authentication Process

Concept quick reference:

Authentication interface: Represents the current user and encapsulates user information.

AuthenticationManager interface: Defines the method for authenticating an Authentication object.

UserDetailsService interface: Core interface for loading user‑specific data (e.g., query by username).

UserDetails interface: Provides core user information; the data returned by UserDetailsService is wrapped into a UserDetails object.

2.3.1 Idea Analysis

Login

① Custom login interface

public ResponseResult login(User user) {
    // call ProviderManager for authentication; if successful, generate JWT and store user info in Redis
}

② Create UserDetailsService implementation to load user from the database.

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    private UserMapper userMapper;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(User::getUserName, username);
        User user = userMapper.selectOne(wrapper);
        if (Objects.isNull(user)) {
            throw new RuntimeException("用户名或密码错误");
        }
        // TODO: query permissions and add to LoginUser
        List<String> perms = Arrays.asList("test");
        return new LoginUser(user, perms);
    }
}

Because UserDetailsService returns a UserDetails, we need a class that implements UserDetails and holds the user and permission data.

@Data
@NoArgsConstructor
public class LoginUser implements UserDetails {
    private User user;
    private List<String> permissions;
    private List<GrantedAuthority> authorities;
    public LoginUser(User user, List<String> permissions) {
        this.user = user;
        this.permissions = permissions;
    }
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        if (authorities != null) return authorities;
        authorities = permissions.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
        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 true; }
    @Override public boolean isCredentialsNonExpired() { return true; }
    @Override public boolean isEnabled() { return true; }
}

2.3.2 Password Encryption Storage

In production, passwords should not be stored in plain text. Spring Security’s PasswordEncoder expects passwords in the format {id}password. We usually replace it with BCryptPasswordEncoder.

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

2.3.3 Login Interface

@RestController
public class LoginController {
    @Autowired
    private LoginService loginService;
    @PostMapping("/user/login")
    public ResponseResult login(@RequestBody User user) {
        return loginService.login(user);
    }
}
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeRequests()
            .antMatchers("/user/login").anonymous()
            .anyRequest().authenticated();
    }
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}
@Service
public class LoginServiceImpl implements LoginService {
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private RedisCache redisCache;
    @Override
    public ResponseResult login(User user) {
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());
        Authentication auth = authenticationManager.authenticate(token);
        if (auth == null) throw new RuntimeException("用户名或密码错误");
        LoginUser loginUser = (LoginUser) auth.getPrincipal();
        String userId = loginUser.getUser().getId().toString();
        String jwt = JwtUtil.createJWT(userId);
        redisCache.setCacheObject("login:" + userId, loginUser);
        HashMap<String, String> map = new HashMap<>();
        map.put("token", jwt);
        return new ResponseResult(200, "登陆成功", map);
    }
    @Override
    public ResponseResult logout() {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser) auth.getPrincipal();
        Long userId = loginUser.getUser().getId();
        redisCache.deleteObject("login:" + userId);
        return new ResponseResult(200, "退出成功");
    }
}

2.3.4 Authentication Filter

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    @Autowired
    private RedisCache redisCache;
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        String token = request.getHeader("token");
        if (!StringUtils.hasText(token)) { chain.doFilter(request, response); return; }
        String userId;
        try {
            Claims claims = JwtUtil.parseJWT(token);
            userId = claims.getSubject();
        } catch (Exception e) { throw new RuntimeException("token非法"); }
        String redisKey = "login:" + userId;
        LoginUser loginUser = redisCache.getCacheObject(redisKey);
        if (loginUser == null) throw new RuntimeException("用户未登录");
        UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(loginUser, null, null);
        SecurityContextHolder.getContext().setAuthentication(authentication);
        chain.doFilter(request, response);
    }
}
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
    @Autowired
    JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeRequests()
            .antMatchers("/user/login").anonymous()
            .anyRequest().authenticated();
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    }
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); }
}

2.3.5 Logout

The logout endpoint removes the user’s data from Redis.

@Service
public class LoginServiceImpl implements LoginService {
    // login method omitted for brevity
    @Override
    public ResponseResult logout() {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser) auth.getPrincipal();
        Long userId = loginUser.getUser().getId();
        redisCache.deleteObject("login:" + userId);
        return new ResponseResult(200, "退出成功");
    }
}

3. Authorization

3.0 Purpose of Permission System

Different users should have access to different functionalities. Front‑end checks alone are insufficient because a client that knows the endpoint URLs could bypass UI restrictions.

3.1 Basic Authorization Flow

Spring Security uses FilterSecurityInterceptor to check permissions. It obtains the current Authentication from SecurityContextHolder and compares the required permissions with those stored in the authentication object.

3.2 Authorization Implementation

3.2.1 Restricting Access to Resources

Enable method‑level security with @EnableGlobalMethodSecurity(prePostEnabled = true) and use @PreAuthorize annotations.

@RestController
public class HelloController {
    @RequestMapping("/hello")
    @PreAuthorize("hasAuthority('test')")
    public String hello() { return "hello"; }
}

3.2.2 Encapsulating Permission Information

Modify LoginUser to hold a list of permission strings and convert them to GrantedAuthority objects.

public class LoginUser implements UserDetails {
    private User user;
    private List<String> permissions;
    private List<GrantedAuthority> authorities;
    public LoginUser(User user, List<String> permissions) { this.user = user; this.permissions = permissions; }
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        if (authorities != null) return authorities;
        authorities = permissions.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
        return authorities;
    }
    // other UserDetails methods omitted for brevity
}

In UserDetailsServiceImpl, query the user’s permissions from the database and construct LoginUser.

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private MenuMapper menuMapper;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(User::getUserName, username);
        User user = userMapper.selectOne(wrapper);
        if (user == null) throw new RuntimeException("用户名或密码错误");
        List<String> permissionKeyList = menuMapper.selectPermsByUserId(user.getId());
        return new LoginUser(user, permissionKeyList);
    }
}

3.2.3 RBAC Permission Model

Roles are linked to menus (permissions) via sys_role_menu. Users are linked to roles via sys_user_role. The effective permission set for a user is obtained by joining these tables.

SQL to retrieve distinct permission strings for a user:

SELECT DISTINCT m.`perms`
FROM sys_user_role ur
LEFT JOIN `sys_role` r ON ur.`role_id` = r.`id`
LEFT JOIN `sys_role_menu` rm ON ur.`role_id` = rm.`role_id`
LEFT JOIN `sys_menu` m ON m.`id` = rm.`menu_id`
WHERE user_id = #{userid}
  AND r.`status` = 0
  AND m.`status` = 0;

Define MenuMapper with a custom method and corresponding XML.

public interface MenuMapper extends BaseMapper<Menu> {
    List<String> selectPermsByUserId(Long id);
}
<mapper namespace="com.sangeng.mapper.MenuMapper">
    <select id="selectPermsByUserId" resultType="java.lang.String">
        SELECT DISTINCT m.`perms`
        FROM sys_user_role ur
        LEFT JOIN `sys_role` r ON ur.`role_id` = r.`id`
        LEFT JOIN `sys_role_menu` rm ON ur.`role_id` = rm.`role_id`
        LEFT JOIN `sys_menu` m ON m.`id` = rm.`menu_id`
        WHERE user_id = #{userid}
          AND r.`status` = 0
          AND m.`status` = 0
    </select>
</mapper>

4. Custom Failure Handling

To return a unified JSON structure on authentication or authorization failures, implement AuthenticationEntryPoint and AccessDeniedHandler and configure them in Spring Security.

@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException ex) throws IOException, ServletException {
        ResponseResult result = new ResponseResult(HttpStatus.FORBIDDEN.value(), "权限不足");
        String json = JSON.toJSONString(result);
        WebUtils.renderString(response, json);
    }
}
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException ex) throws IOException, ServletException {
        ResponseResult result = new ResponseResult(HttpStatus.UNAUTHORIZED.value(), "认证失败请重新登录");
        String json = JSON.toJSONString(result);
        WebUtils.renderString(response, json);
    }
}
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private AuthenticationEntryPoint authenticationEntryPoint;
    @Autowired
    private AccessDeniedHandler accessDeniedHandler;
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.exceptionHandling()
            .authenticationEntryPoint(authenticationEntryPoint)
            .accessDeniedHandler(accessDeniedHandler);
        // other configurations omitted for brevity
    }
}

5. Cross‑Origin (CORS)

Configure Spring MVC to allow cross‑origin requests.

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOriginPatterns("*")
                .allowCredentials(true)
                .allowedMethods("GET", "POST", "DELETE", "PUT")
                .allowedHeaders("*")
                .maxAge(3600);
    }
}

Enable CORS in Spring Security.

http.cors();

6. Miscellaneous Topics

6.1 Other Permission Check Methods

Spring Security provides hasAnyAuthority, hasRole, hasAnyRole, etc. hasRole and hasAnyRole automatically prepend ROLE_ to the supplied role name.

@PreAuthorize("hasAnyAuthority('admin','test','system:dept:list')")
public String hello() { return "hello"; }

@PreAuthorize("hasRole('ADMIN')")
public String adminOnly() { return "admin"; }

6.2 Custom Permission Evaluation

Define a bean that provides custom methods for SpEL expressions.

@Component("ex")
public class SGExpressionRoot {
    public boolean hasAuthority(String authority) {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser) auth.getPrincipal();
        return loginUser.getPermissions().contains(authority);
    }
}

Use it in annotations:

@PreAuthorize("@ex.hasAuthority('system:dept:list')")
public String hello() { return "hello"; }

6.3 Configuration‑Based Permission Control

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
        .antMatchers("/user/login").anonymous()
        .antMatchers("/testCors").hasAuthority("system:dept:list222")
        .anyRequest().authenticated();
    // other configurations omitted
}

6.4 CSRF

CSRF protection relies on a token stored in a cookie. In stateless JWT‑based APIs, CSRF is not a concern because authentication does not use cookies.

6.5 Authentication Success & Failure Handlers

@Component
public class SGSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        System.out.println("认证成功了");
    }
}

@Component
public class SGFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        System.out.println("认证失败了");
    }
}
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private AuthenticationSuccessHandler successHandler;
    @Autowired
    private AuthenticationFailureHandler failureHandler;
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin().successHandler(successHandler).failureHandler(failureHandler);
        http.authorizeRequests().anyRequest().authenticated();
    }
}

6.6 Logout Success Handler

@Component
public class SGLogoutSuccessHandler implements LogoutSuccessHandler {
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        System.out.println("注销成功");
    }
}
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private LogoutSuccessHandler logoutSuccessHandler;
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.logout().logoutSuccessHandler(logoutSuccessHandler);
        http.authorizeRequests().anyRequest().authenticated();
    }
}

6.7 Future Authentication Schemes

The diagram below illustrates possible extensions such as OAuth2, SSO, or custom token strategies.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

redisSpring BootAuthenticationJWTAuthorizationRBACspring-security
Java High-Performance Architecture
Written by

Java High-Performance Architecture

Sharing Java development articles and resources, including SSM architecture and the Spring ecosystem (Spring Boot, Spring Cloud, MyBatis, Dubbo, Docker), Zookeeper, Redis, architecture design, microservices, message queues, Git, etc.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.