How to Build a Custom Spring Security Authentication Flow with Redis Token Management
This article walks through the complete process of implementing a custom login authentication in Spring Boot, including custom authentication filters, success/failure handlers, a token stored in Redis, role‑based URL security, and detailed configuration of Spring Security headers and session handling.
First, the article defines the functional requirements: custom login authentication, token generation stored in Redis, and per‑API role‑based permission checks.
Environment Setup
Add the following Maven dependencies for Spring Security, Spring Session with Redis, and Spring Data Redis (Spring Boot version 2.3.4.RELEASE is used):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.3.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
<version>2.3.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.3.4.RELEASE</version>
</dependency>WebSecurityConfig
Create WebSecurityConfig.java extending WebSecurityConfigurerAdapter. The class ignores static resources, configures form‑login, and registers a custom LoginFilter:
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired private UserVerifyAuthenticationProvider authenticationManager;
@Autowired private CustomAuthenticationSuccessHandler successHandler;
@Autowired private CustomAuthenticationFailureHandler failureHandler;
@Autowired private MyFilterInvocationSecurityMetadataSource securityMetadataSource;
@Autowired private MyAccessDecisionManager accessDecisionManager;
@Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers("/*.html","/favicon.ico","/**/*.html","/**/*.css","/**/*.js","/error","/webjars/**","/resources/**","/swagger-ui.html","/swagger-resources/**","/v2/api-docs");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/demo/**","/about/**").permitAll()
.anyRequest().authenticated()
.and()
.addFilter(new LoginFilter(authenticationManager, successHandler, failureHandler))
.csrf().disable()
.exceptionHandling()
.authenticationEntryPoint(new CustomAuthenticationEntryPoint())
.accessDeniedHandler(new CustomAccessDeniedHandler())
.and()
.headers()
.contentTypeOptions().and()
.xssProtection().and()
.cacheControl().and()
.httpStrictTransportSecurity().and()
.frameOptions().disable();
}
@Bean
public HttpSessionIdResolver httpSessionIdResolver() {
return HeaderHttpSessionIdResolver.xAuthToken();
}
}Custom Handlers
Implement CustomAuthenticationSuccessHandler and CustomAuthenticationFailureHandler to return JSON responses and log the result. Both classes implement the corresponding Spring Security interfaces and write a JSON payload to the response.
@Component
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(CustomAuthenticationSuccessHandler.class);
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
String responseJson = JackJsonUtil.object2String(ResponseFactory.success(authentication));
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("登录成功!");
}
response.getWriter().write(responseJson);
}
} @Component
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(CustomAuthenticationFailureHandler.class);
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException {
String errorMsg = StringUtils.isNotBlank(e.getMessage()) ? e.getMessage() : CodeMsgEnum.LOG_IN_FAIL.getMsg();
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
String responseJson = JackJsonUtil.object2String(ResponseFactory.fail(CodeMsgEnum.LOG_IN_FAIL, errorMsg));
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("认证失败!");
}
response.getWriter().write(responseJson);
}
}Authentication Provider
Implement UserVerifyAuthenticationProvider (implements AuthenticationProvider) to validate the username and password against the database, decode Base64‑encoded passwords, compare using an MD5 encoder, and build a UsernamePasswordAuthenticationToken with the user's roles.
@Component
public class UserVerifyAuthenticationProvider implements AuthenticationProvider {
private PasswordEncoder passwordEncoder;
@Autowired private UserService userService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String userName = (String) authentication.getPrincipal();
String passWord = (String) authentication.getCredentials();
UserRoleVo userRoleVo = userService.findUserRoleByAccount(userName);
String encodedPassword = userRoleVo.getPassWord();
String credPassword = new String(Base64Utils.decodeFromString(passWord), StandardCharsets.UTF_8);
passwordEncoder = new MD5Util();
if (!passwordEncoder.matches(credPassword, encodedPassword)) {
throw new AuthenticationServiceException("账号或密码错误!");
}
List<GrantedAuthority> roles = new LinkedList<>();
for (Role role : userRoleVo.getRoleList()) {
roles.add(new SimpleGrantedAuthority(role.getRoleId().toString()));
}
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(userName, passWord, roles);
token.setDetails(userRoleVo);
return token;
}
@Override
public boolean supports(Class<?> authentication) {
return false;
}
}Login Filter
Extend UsernamePasswordAuthenticationFilter to read JSON login data, delegate authentication to the custom provider, and set the filter URL to /myLogin:
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
private UserVerifyAuthenticationProvider authenticationManager;
public LoginFilter(UserVerifyAuthenticationProvider authenticationManager, CustomAuthenticationSuccessHandler successHandler, CustomAuthenticationFailureHandler failureHandler) {
this.authenticationManager = authenticationManager;
this.setAuthenticationSuccessHandler(successHandler);
this.setAuthenticationFailureHandler(failureHandler);
super.setFilterProcessesUrl("/myLogin");
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
UserDTO loginUser = new ObjectMapper().readValue(request.getInputStream(), UserDTO.class);
return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(loginUser.getUserName(), loginUser.getPassWord()));
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
}Role‑Based URL Security
Two custom components are introduced:
MyFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource. It loads all URL‑role mappings from the database and uses AntPathRequestMatcher to match the incoming request, returning the list of roles that may access the URL.
MyAccessDecisionManager implements AccessDecisionManager. It receives the required roles from the metadata source and checks whether any of the authenticated user's authorities match; if none match, an AccessDeniedException is thrown.
@Component
public class MyFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
@Autowired private RoleService roleService;
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
FilterInvocation fi = (FilterInvocation) object;
HttpServletRequest request = fi.getRequest();
List<Map<String, String>> allUrlRoleMap = roleService.getAllUrlRoleMap();
for (Map<String, String> urlRoleMap : allUrlRoleMap) {
String url = urlRoleMap.get("url");
String roles = urlRoleMap.get("roles");
AntPathRequestMatcher matcher = new AntPathRequestMatcher(url);
if (matcher.matches(request)) {
return SecurityConfig.createList(roles.split(","));
}
}
return null;
}
// other methods omitted for brevity
} @Component
public class MyAccessDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
for (ConfigAttribute attribute : configAttributes) {
String needCode = attribute.getAttribute();
for (GrantedAuthority authority : authentication.getAuthorities()) {
if (StringUtils.equals(authority.getAuthority(), needCode)) {
return;
}
}
}
throw new AccessDeniedException("当前访问没有权限");
}
// other methods omitted for brevity
}Exception Handling for Anonymous and Authenticated Users
Two handlers are added to the security chain: CustomAuthenticationEntryPoint returns a JSON error when an unauthenticated (anonymous) user accesses a protected resource. CustomAccessDeniedHandler returns a JSON error when an authenticated user lacks the required role.
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
private static final Logger LOGGER = LoggerFactory.getLogger(CustomAuthenticationEntryPoint.class);
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
String message = JackJsonUtil.object2String(ResponseFactory.fail(MOVED_PERMANENTLY, MessageConstant.NOT_LOGGED_IN));
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("未登录重定向!");
}
response.getWriter().write(message);
}
} @Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(CustomAccessDeniedHandler.class);
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
String message = JackJsonUtil.object2String(ResponseFactory.fail(CodeMsgEnum.UNAUTHORIZED, MessageConstant.NO_ACCESS));
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("没有权限访问!");
}
response.getWriter().write(message);
}
}Session Token in Header
Configure HttpSessionIdResolver to use the x-auth-token header. The session is stored in Redis with a 30‑minute timeout:
session:
store-type: redis
redis:
namespace: spring:session:admin
timeout: 1800Security headers are also configured (Content‑Type Options, XSS protection, cache control, HSTS, and frame options disabled).
Testing and Observations
After deploying, a POST to /myLogin returns a JSON response containing the token in the x-auth-token header; the token is persisted in Redis (verified via Redis client). Subsequent API calls include the header to authenticate.
Issues and Adjustments
The author notes that loading all URL‑role mappings on every request simplifies dynamic permission updates but may affect performance. They also discuss a conflict where URLs configured as publicly accessible in WebSecurityConfig were still processed by the custom metadata source, leading to unexpected access control behavior. The workaround shown moves the public‑URL configuration into the custom metadata source.
Overall, the article provides a thorough, step‑by‑step guide to building a flexible, token‑based authentication system with fine‑grained permission control in Spring Boot.
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.
Architect
Professional architect sharing high‑quality architecture insights. Topics include high‑availability, high‑performance, high‑stability architectures, big data, machine learning, Java, system and distributed architecture, AI, and practical large‑scale architecture case studies. Open to ideas‑driven architects who enjoy sharing and learning.
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.
