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.
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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
