Master Spring Security: Quick Start, JWT Authentication, and RBAC Authorization
This comprehensive guide walks you through setting up Spring Security in a Spring Boot project, configuring password encoding, implementing JWT-based authentication, building custom login and logout endpoints, managing user details with MyBatis Plus, and applying role‑based access control with custom permission handlers, all illustrated with complete code examples.
Content Introduction
Spring Security is a security management framework in the Spring ecosystem, offering richer features and a larger community than Shiro. It is commonly used for authentication and authorization in medium to large projects.
1. Quick Start
1.1 Preparation
Set up a simple Spring Boot project.
<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 simple controller:
@RestController
public class HelloController {
@RequestMapping("/hello")
public String hello() {
return "hello";
}
}1.2 Adding Spring Security
Add the dependency:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>After adding the dependency, accessing the endpoint redirects to the default login page.
2. Authentication
2.1 Login Flow
2.2 Core Concepts
Spring Security uses a filter chain. Key filters include UsernamePasswordAuthenticationFilter for login requests, ExceptionTranslationFilter for handling exceptions, and FilterSecurityInterceptor for permission checks.
Typical filter chain diagram:
2.3.1 User Authentication
Define a UserDetailsService implementation that loads user data 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: load permissions
List<String> permissions = new ArrayList<>(Arrays.asList("test"));
return new LoginUser(user, permissions);
}
}2.3.2 Password Encryption
Use BCryptPasswordEncoder for password hashing.
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}2.3.3 Login Endpoint
@RestController
public class LoginController {
@Autowired
private LoginService loginService;
@PostMapping("/user/login")
public ResponseResult login(@RequestBody User user) {
return loginService.login(user);
}
}Login service generates JWT and stores user details in Redis.
@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 (Objects.isNull(auth)) {
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 authentication = SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
Long userid = loginUser.getUser().getId();
redisCache.deleteObject("login:" + userid);
return new ResponseResult(200, "退出成功");
}
}2.3.4 JWT Authentication Filter
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private RedisCache redisCache;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = request.getHeader("token");
if (!StringUtils.hasText(token)) {
filterChain.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 (Objects.isNull(loginUser)) {
throw new RuntimeException("用户未登录");
}
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(request, response);
}
}2.4 Security Configuration
@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();
}
}3. Authorization
3.1 Permission System
Different users should have access to different functionalities. Use method‑level annotations such as @PreAuthorize to enforce permissions.
3.2 Enabling Annotations
@EnableGlobalMethodSecurity(prePostEnabled = true)Example:
@RestController
public class HelloController {
@RequestMapping("/hello")
@PreAuthorize("hasAuthority('test')")
public String hello() {
return "hello";
}
}3.2.1 Adding Permissions to UserDetails
Extend 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
}3.2.2 Loading Permissions from Database (RBAC)
Database tables sys_user, sys_role, sys_menu, sys_user_role, and sys_role_menu implement a classic RBAC model.
Mapper to fetch distinct permission strings for a user:
public interface MenuMapper extends BaseMapper<Menu> {
List<String> selectPermsByUserId(Long id);
}Corresponding XML:
<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>Update UserDetailsServiceImpl to load permissions:
@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 (Objects.isNull(user)) {
throw new RuntimeException("用户名或密码错误");
}
List<String> permissionKeyList = menuMapper.selectPermsByUserId(user.getId());
return new LoginUser(user, permissionKeyList);
}
}3.3 Custom Failure Handlers
Define JSON responses for authentication and authorization failures.
@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);
}
}Inject them into the security configuration:
@Autowired
private AuthenticationEntryPoint authenticationEntryPoint;
@Autowired
private AccessDeniedHandler accessDeniedHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler);
// other configurations
http.cors();
}3.4 CORS Support
Global CORS configuration:
@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();3.5 Additional Topics
Other authorization expressions include hasAnyAuthority, hasRole, and hasAnyRole. Custom SPEL methods can be defined in a bean and referenced as @ex.hasAuthority('perm'). Configuration‑based URL security can be set with .antMatchers(...).hasAuthority(...). CSRF protection is typically disabled for stateless JWT APIs because tokens are sent in headers, not cookies.
3.6 Custom Authentication Handlers
Implement AuthenticationSuccessHandler and AuthenticationFailureHandler for custom login responses, and LogoutSuccessHandler for logout handling.
@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("认证失败了");
}
} @Component
public class SGLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
System.out.println("注销成功");
}
}Wire them into SecurityConfig via .formLogin().successHandler(...).failureHandler(...) and .logout().logoutSuccessHandler(...).
4. Summary
This article provides a step‑by‑step implementation of Spring Security with JWT, Redis session storage, RBAC permission loading, method‑level security annotations, custom error handling, and CORS configuration, offering a solid foundation for building secure Java backend applications.
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.
