Secure Your Spring Boot APIs with JWT: A Step‑by‑Step Guide Using Spring Security
This article explains how to replace Shiro with Spring Security, integrate JWT for stateless authentication, configure the filter chain, set up the necessary Maven dependencies, write the security configuration, custom authentication and verification filters, and manage user details in a Spring Boot backend.
As a backend developer you’re familiar with permissions such as public and private. While many projects use Shiro, this guide shows how to use Spring Security together with JWT for authentication and authorization in a Spring Boot application.
Introduction
Two core concepts:
Authentication ( Authentication): the system verifies a username and password to confirm the user is a legitimate subject.
Authorization ( Authorization): the system assigns roles or permissions to the user, determining whether the user can perform a specific operation.
Spring Security, built on the Spring framework, provides seamless integration, comprehensive permission control, and is designed specifically for web development.
Principle
Spring Security works as a chain of filter objects. The diagram below (green part) can be configured, while the orange and blue parts are fixed.
FilterSecurityInterceptor: the final filter that decides whether the current request can access the target Controller. ExceptionTranslationFilter: handles exceptions and redirects the user to the authentication process.
Practical Implementation
Project Preparation
We use the Spring Boot framework. Add the following dependencies to pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- Alibaba JSON parser -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.74</version>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.10.6</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>Configure application.yml:
spring:
application:
name: securityjwt
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/cheetah?characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
username: root
password: 123456
server:
port: 8080
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.itcheetah.securityjwt.entity
configuration:
map-underscore-to-camel-case: true
rsa:
key:
pubKeyFile: C:\Users\Desktop\jwt\id_key_rsa.pub
priKeyFile: C:\Users\Desktop\jwt\id_key_rsaCreate the required tables (SQL snippets omitted for brevity).
Dependency Injection
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Token generation and parsing -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>After adding the dependencies the application starts and shows the login page (image).
SecurityConfig Class
@EnableGlobalMethodSecurity(prePostEnabled=true, securedEnabled=true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AuthenticationEntryPointImpl unauthorizedHandler;
@Autowired
private RsaKeyProperties prop;
@Autowired
private UserInfoService userInfoService;
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.csrf().disable()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js").permitAll()
.anyRequest().authenticated()
.and()
.headers().frameOptions().disable();
httpSecurity.addFilter(new TokenLoginFilter(super.authenticationManager(), prop))
.addFilter(new TokenVerifyFilter(super.authenticationManager(), prop));
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userInfoService).passwordEncoder(passwordEncoder());
}
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}Interception rules (anyRequest, access, anonymous, denyAll, fullyAuthenticated, hasAnyAuthority, hasAnyRole, hasAuthority, hasIpAddress, hasRole, permitAll, rememberMe, authenticated) are listed in the original article.
Authentication Failure Handler
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable {
private static final long serialVersionUID = -8970718410437077606L;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException {
int code = HttpStatus.UNAUTHORIZED;
String msg = "Authentication failed, unable to access system resources, please log in first";
ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg)));
}
}Authentication Process
Custom Authentication Filter
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
private RsaKeyProperties prop;
public TokenLoginFilter(AuthenticationManager authenticationManager, RsaKeyProperties prop) {
this.authenticationManager = authenticationManager;
this.prop = prop;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
UserPojo sysUser = new ObjectMapper().readValue(request.getInputStream(), UserPojo.class);
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(sysUser.getUsername(), sysUser.getPassword());
return authenticationManager.authenticate(authRequest);
} catch (Exception e) {
try {
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
PrintWriter out = response.getWriter();
Map resultMap = new HashMap();
resultMap.put("code", HttpServletResponse.SC_UNAUTHORIZED);
resultMap.put("msg", "用户名或密码错误!");
out.write(new ObjectMapper().writeValueAsString(resultMap));
out.flush();
out.close();
} catch (Exception outEx) {
outEx.printStackTrace();
}
throw new RuntimeException(e);
}
}
@Override
public void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
UserPojo user = new UserPojo();
user.setUsername(authResult.getName());
user.setRoles((List<RolePojo>)authResult.getAuthorities());
String token = JwtUtils.generateTokenExpireInMinutes(user, prop.getPrivateKey(), 24 * 60);
response.addHeader("Authorization", "Bearer " + token);
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_OK);
PrintWriter out = response.getWriter();
Map resultMap = new HashMap();
resultMap.put("code", HttpServletResponse.SC_OK);
resultMap.put("msg", "认证通过!");
resultMap.put("token", token);
out.write(new ObjectMapper().writeValueAsString(resultMap));
out.flush();
out.close();
}
}The default login URL is /login. When this endpoint is called, Spring Security invokes attemptAuthentication (illustrated by the flow diagrams below).
Custom UserInfoService
public interface UserInfoService extends UserDetailsService {}
@Service
@Transactional
public class UserInfoServiceImpl implements UserInfoService {
@Autowired
private SysUserInfoMapper userInfoMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserPojo user = userInfoMapper.queryByUserName(username);
return user;
}
} UserPojoimplements UserDetails and provides authorities, password, username, and the required boolean flags.
@Data
public class UserPojo implements UserDetails {
private Integer id;
private String username;
private String password;
private Integer status;
private List<RolePojo> roles;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SimpleGrantedAuthority> auth = new ArrayList<>();
auth.add(new SimpleGrantedAuthority("ADMIN"));
return auth;
}
@Override public String getPassword() { return this.password; }
@Override public String getUsername() { return this.username; }
@Override public boolean isAccountNonExpired() { return true; }
@Override public boolean isAccountNonLocked() { return true; }
@Override public boolean isCredentialsNonExpired() { return true; }
@Override public boolean isEnabled() { return true; }
}Token Verification Filter
public class TokenVerifyFilter extends BasicAuthenticationFilter {
private RsaKeyProperties prop;
public TokenVerifyFilter(AuthenticationManager authenticationManager, RsaKeyProperties prop) {
super(authenticationManager);
this.prop = prop;
}
@Override
public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String header = request.getHeader("Authorization");
if (header == null || !header.startsWith("Bearer ")) {
chain.doFilter(request, response);
} else {
String token = header.replace("Bearer ", "");
Payload<UserPojo> payload = JwtUtils.getInfoFromToken(token, prop.getPublicKey(), UserPojo.class);
UserPojo user = payload.getUserInfo();
if (user != null) {
UsernamePasswordAuthenticationToken authResult = new UsernamePasswordAuthenticationToken(user.getUsername(), null, user.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authResult);
chain.doFilter(request, response);
}
}
}
}When accessing protected resources the client must include the JWT in the Authorization header (Bearer token) as shown in the diagram.
Overall Flow Diagram
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.
macrozheng
Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.
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.
