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.
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: NonceJwtSecretDatabase 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;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.
