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.

Architect
Architect
Architect
How to Build a Custom Spring Security Authentication Flow with Redis Token Management

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: 1800

Security 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.

Environment Setup
Environment Setup
Custom Login Authentication
Custom Login Authentication
Login Request Flow
Login Request Flow
Login Success Token
Login Success Token
Redis Storage Confirmation
Redis Storage Confirmation

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.

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.

javaredisSpring Boottoken managementspring-securityCustom Authentication
Architect
Written by

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.

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.