Master Spring Security: JWT, Custom Handlers, and Dynamic Permissions

This article provides a comprehensive guide to configuring Spring Security in a Java backend, covering Maven dependencies, global security settings, JWT token parsing, custom authentication providers, user details services, dynamic URL permission metadata, access decision management, and custom handlers for login, logout, and error responses, along with sample application.yml and database schema.

Java High-Performance Architecture
Java High-Performance Architecture
Java High-Performance Architecture
Master Spring Security: JWT, Custom Handlers, and Dynamic Permissions

Preface

Spring Security is a powerful, highly customizable authentication and access‑control framework that has become the de‑facto standard for protecting Spring‑based Java applications.

Project Setup

Dependency Introduction

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

Global Login Interception Configuration

@Configuration
public class WebSecurityConfigure extends WebSecurityConfigurerAdapter {

    @Resource
    private UrlAuthenticationEntryPoint authenticationEntryPoint; // 401 when not logged in
    @Resource
    private UrlAuthenticationSuccessHandler authenticationSuccessHandler; // 200 + token
    @Resource
    private UrlAuthenticationFailureHandler authenticationFailureHandler; // 402
    @Resource
    private UrlAccessDeniedHandler accessDeniedHandler; // 403
    @Resource
    private UrlLogoutSuccessHandler logoutSuccessHandler; // 200
    @Resource
    private SelfAuthenticationProvider authenticationProvider;
    @Resource
    private SelfFilterInvocationSecurityMetadataSource filterInvocationSecurityMetadataSource;
    @Resource
    private SelfAccessDecisionManager accessDecisionManager;
    @Resource
    private AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> authenticationDetailsSource;
    @Resource
    private JwtAuthorizationTokenFilter authorizationTokenFilter;

    @Override
    public void configure(WebSecurity web) {
        web.ignoring().antMatchers("/common/**");
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) {
        auth.authenticationProvider(authenticationProvider);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();
        http.addFilterBefore(authorizationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
        http.exceptionHandling().accessDeniedHandler(accessDeniedHandler);
        http.antMatcher("/**").authorizeRequests()
            .anyRequest().authenticated()
            .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                @Override
                public <O extends FilterSecurityInterceptor> O postProcess(O o) {
                    o.setSecurityMetadataSource(filterInvocationSecurityMetadataSource);
                    o.setAccessDecisionManager(accessDecisionManager);
                    return o;
                }
            });
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        http.formLogin()
            .loginProcessingUrl("/nonceLogin")
            .usernameParameter("username")
            .passwordParameter("password")
            .successHandler(authenticationSuccessHandler)
            .failureHandler(authenticationFailureHandler)
            .authenticationDetailsSource(authenticationDetailsSource);
        http.logout()
            .logoutUrl("/nonceLogout")
            .logoutSuccessHandler(logoutSuccessHandler);
    }
}

JWT Token Filter

@SuppressWarnings("unchecked")
@Slf4j
@Component
public class JwtAuthorizationTokenFilter extends OncePerRequestFilter {

    @Value("${jwt.token-header-key}")
    private String tokenHeaderKey;
    @Value("${jwt.token-prefix}")
    private String tokenPrefix;
    @Value("${jwt.token-secret}")
    private String tokenSecret;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {
        String token = request.getHeader(tokenHeaderKey);
        log.info("JwtAuthorizationTokenFilter >> token:{}", token);
        if (token == null || !token.startsWith(tokenPrefix + " ")) {
            chain.doFilter(request, response);
            return;
        }
        Claims claims;
        try {
            claims = Jwts.parser().setSigningKey(tokenSecret)
                    .parseClaimsJws(token.replace(tokenPrefix + " ", "")).getBody();
        } catch (Exception e) {
            log.error("JwtToken validity!! error={}", e.getMessage());
            chain.doFilter(request, response);
            return;
        }
        String username = claims.getSubject();
        List<String> roles = claims.get("role", List.class);
        List<SimpleGrantedAuthority> authorities = roles.stream()
                .map(SimpleGrantedAuthority::new).collect(Collectors.toList());
        if (username != null) {
            UsernamePasswordAuthenticationToken authentication =
                    new UsernamePasswordAuthenticationToken(username, null, authorities);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        chain.doFilter(request, response);
    }
}

Custom Authentication Provider

@Slf4j
@Component
public class SelfAuthenticationProvider implements AuthenticationProvider {

    @Resource
    private SelfUserDetailsService selfUserDetailsService;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        CustomWebAuthenticationDetails details = (CustomWebAuthenticationDetails) authentication.getDetails();
        System.out.println("macAddress >> " + details.getMacAddress());
        String username = (String) authentication.getPrincipal();
        String password = (String) authentication.getCredentials();
        UserDetails userInfo = selfUserDetailsService.loadUserByUsername(username);
        boolean matches = new BCryptPasswordEncoder().matches(password, userInfo.getPassword());
        if (!matches) {
            throw new BadCredentialsException("The password is incorrect!!");
        }
        return new UsernamePasswordAuthenticationToken(username, userInfo.getPassword(), userInfo.getAuthorities());
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }
}

Custom UserDetails Service

@Component
public class SelfUserDetailsService implements UserDetailsService {

    @Resource
    private UserService userService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        SelfUserDetails userInfo = new SelfUserDetails();
        userInfo.setUsername(username);
        String password = userService.findPasswordByUsernameAfterValidTime(username);
        if (ObjectUtils.isEmpty(password)) {
            throw new UsernameNotFoundException("User name " + username + " not find!!");
        }
        userInfo.setPassword(password);
        Set<SimpleGrantedAuthority> authoritiesSet = new HashSet<>();
        List<String> roles = userService.findRoleNameByUsername(username);
        for (String roleName : roles) {
            authoritiesSet.add(new SimpleGrantedAuthority(roleName));
        }
        userInfo.setAuthorities(authoritiesSet);
        return userInfo;
    }
}

Custom WebAuthenticationDetails

class CustomWebAuthenticationDetails extends WebAuthenticationDetails implements Serializable {

    private String macAddress;

    CustomWebAuthenticationDetails(HttpServletRequest request) {
        super(request);
        macAddress = request.getParameter("macAddress");
    }

    String getMacAddress() {
        return macAddress;
    }
}

Custom AuthenticationDetailsSource

@Component
public class CustomAuthenticationDetailsSource implements AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> {

    @Override
    public WebAuthenticationDetails buildDetails(HttpServletRequest request) {
        return new CustomWebAuthenticationDetails(request);
    }
}

Dynamic URL Permission Metadata Source

@Slf4j
@Component
public class SelfFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

    @Resource
    private UserService userService;

    private AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        Set<ConfigAttribute> set = new HashSet<>();
        String requestUrl = ((FilterInvocation) object).getRequestUrl();
        log.info("requestUrl >> {}", requestUrl);
        List<String> menuUrl = userService.findAllMenuUrl();
        for (String url : menuUrl) {
            if (antPathMatcher.match(url, requestUrl)) {
                List<String> roleNames = userService.findRoleNameByMenuUrl(url);
                roleNames.forEach(roleName -> set.add(new SecurityConfig(roleName)));
            }
        }
        if (ObjectUtils.isEmpty(set)) {
            return SecurityConfig.createList("ROLE_LOGIN");
        }
        return set;
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return FilterInvocation.class.isAssignableFrom(aClass);
    }
}

Custom AccessDecisionManager

@Slf4j
@Component
public class SelfAccessDecisionManager implements AccessDecisionManager {

    @Override
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> collection)
            throws AccessDeniedException, InsufficientAuthenticationException {
        log.info("collection:{}", collection);
        Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
        log.info("principal:{} authorities:{}", authentication.getPrincipal().toString(), authorities);
        for (ConfigAttribute configAttribute : collection) {
            String needRole = configAttribute.getAttribute();
            if ("ROLE_LOGIN".equals(needRole)) {
                if (authentication instanceof AnonymousAuthenticationToken) {
                    throw new BadCredentialsException("Not logged in!!");
                } else {
                    return;
                }
            }
            for (GrantedAuthority grantedAuthority : authorities) {
                if (grantedAuthority.getAuthority().equals(needRole)) {
                    return;
                }
            }
        }
        throw new AccessDeniedException("SimpleGrantedAuthority!!");
    }

    @Override
    public boolean supports(ConfigAttribute configAttribute) {
        return true;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }
}

Custom Handlers

401 – Unauthenticated

@Component
public class UrlAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e)
            throws IOException, ServletException {
        UrlResponse resp = new UrlResponse();
        resp.setSuccess(false);
        resp.setCode("401");
        resp.setMessage(e.getMessage());
        resp.setData(null);
        response.setStatus(401);
        response.setCharacterEncoding("UTF-8");
        response.setContentType("text/html;charset=UTF-8");
        response.getWriter().write(GsonUtil.GSON.toJson(resp));
    }
}

200 – Login Success (returns token)

@Component
public class UrlAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Value("${jwt.token-header-key}")
    private String tokenHeaderKey;
    @Value("${jwt.token-prefix}")
    private String tokenPrefix;
    @Value("${jwt.token-secret}")
    private String tokenSecret;
    @Value("${jwt.token-expiration}")
    private Long tokenExpiration;
    @Resource
    private UserService userService;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws IOException, ServletException {
        response.setCharacterEncoding("UTF-8");
        UrlResponse resp = new UrlResponse();
        resp.setSuccess(true);
        resp.setCode("200");
        resp.setMessage("Login Success!");
        String username = (String) authentication.getPrincipal();
        Map<String, Object> userInfo = userService.findMenuInfoByUsername(username, resp);
        resp.setData(userInfo);
        Claims claims = Jwts.claims();
        claims.put("role", authentication.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority).collect(Collectors.toList()));
        String token = Jwts.builder()
                .setClaims(claims)
                .setSubject(username)
                .setExpiration(new Date(System.currentTimeMillis() + tokenExpiration))
                .signWith(SignatureAlgorithm.HS512, tokenSecret).compact();
        response.addHeader(tokenHeaderKey, tokenPrefix + " " + token);
        response.setContentType("text/html;charset=UTF-8");
        response.getWriter().write(GsonUtil.GSON.toJson(resp));
    }
}

402 – Login Failure

@Component
public class UrlAuthenticationFailureHandler implements AuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                                        AuthenticationException e) throws IOException, ServletException {
        UrlResponse resp = new UrlResponse();
        resp.setSuccess(false);
        resp.setCode("402");
        resp.setMessage(e.getMessage());
        resp.setData(null);
        response.setStatus(402);
        response.setCharacterEncoding("UTF-8");
        response.setContentType("text/html;charset=UTF-8");
        response.getWriter().write(GsonUtil.GSON.toJson(resp));
    }
}

403 – Access Denied

@Component
public class UrlAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e)
            throws IOException, ServletException {
        UrlResponse resp = new UrlResponse();
        resp.setSuccess(false);
        resp.setCode("403");
        resp.setMessage(e.getMessage());
        resp.setData(null);
        response.setStatus(403);
        response.setCharacterEncoding("UTF-8");
        response.setContentType("text/html;charset=UTF-8");
        response.getWriter().write(GsonUtil.GSON.toJson(resp));
    }
}

200 – Logout Success

@Component
public class UrlLogoutSuccessHandler implements LogoutSuccessHandler {

    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response,
                                Authentication authentication) throws IOException, ServletException {
        UrlResponse resp = new UrlResponse();
        resp.setSuccess(true);
        resp.setCode("200");
        resp.setMessage("Logout Success!!");
        resp.setData(null);
        response.setCharacterEncoding("UTF-8");
        response.setContentType("text/html;charset=UTF-8");
        response.getWriter().write(GsonUtil.GSON.toJson(resp));
    }
}

application.yml

server:
  port: 8018
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2b8&autoReconnect=true&failOverReadOnly=false
    username: root
    password: root
    driver-class-name: com.mysql.jdbc.Driver
    hikari:
      read-only: false
      connection-timeout: 60000
      idle-timeout: 60000
      validation-timeout: 3000
      max-lifetime: 60000
      login-timeout: 5
      maximum-pool-size: 60
      minimum-idle: 10
  jpa:
    generate-ddl: false
    show-sql: false
    hibernate:
      ddl-auto: none
    database: mysql
    open-in-view: true
jwt:
  token-header-key: Authorization
  token-prefix: NonceToken
  token-expiration: 43200000
  token-secret: NonceJwtSecret

Database Table Design

SET FOREIGN_KEY_CHECKS=0;

DROP TABLE IF EXISTS `authority_user`;
CREATE TABLE `authority_user` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `username` varchar(255) DEFAULT NULL COMMENT '用户名',
  `password` varchar(255) DEFAULT NULL COMMENT '密码',
  `email` varchar(255) DEFAULT NULL COMMENT '邮箱',
  `phone` varchar(255) DEFAULT NULL COMMENT '手机号',
  `valid_time` varchar(255) DEFAULT NULL COMMENT '有效截止时间',
  `update_time` varchar(255) DEFAULT NULL COMMENT '更新时间',
  `remark` mediumtext COMMENT '备注',
  `nickname` varchar(255) DEFAULT NULL COMMENT '昵称',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=38 DEFAULT CHARSET=utf8;

DROP TABLE IF EXISTS `authority_role`;
CREATE TABLE `authority_role` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `role_name` varchar(255) DEFAULT NULL COMMENT '角色名称(必须以ROLE_起始命名)',
  `role_name_CN` varchar(255) DEFAULT NULL COMMENT '角色名称中文',
  `update_time` varchar(255) DEFAULT NULL COMMENT '更新时间',
  `remark` varchar(255) DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=30 DEFAULT CHARSET=utf8;

DROP TABLE IF EXISTS `authority_menu`;
CREATE TABLE `authority_menu` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `url` varchar(255) DEFAULT NULL COMMENT '请求路径',
  `menu_name` varchar(255) DEFAULT NULL COMMENT '菜单名称',
  `parent_id` int(11) DEFAULT NULL COMMENT '父菜单id',
  `update_time` varchar(255) DEFAULT NULL COMMENT '更新时间',
  `remark` varchar(255) DEFAULT NULL COMMENT '备注',
  `url_pre` varchar(255) DEFAULT NULL COMMENT '路由(前端自己匹配用)',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=73 DEFAULT CHARSET=utf8;

DROP TABLE IF EXISTS `authority_user_role`;
CREATE TABLE `authority_user_role` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `user_id` int(11) DEFAULT NULL,
  `role_id` int(11) DEFAULT NULL,
  `update_time` varchar(255) DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=63 DEFAULT CHARSET=utf8;

DROP TABLE IF EXISTS `authority_role_menu`;
CREATE TABLE `authority_role_menu` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `role_id` int(11) DEFAULT NULL,
  `menu_id` int(11) DEFAULT NULL,
  `update_time` varchar(255) DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=798 DEFAULT CHARSET=utf8;
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.

JavaSpring BootAuthenticationJWTAuthorizationspring-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.