Firstly I want to define some terminology used in the spring security context.
- Authentication: verifying identity of a user based on provided credentials.
- Authorization: ensuring if a user has proper permission to perform a particular action or read some data.
Folder structure:
Dependencies used:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
To learn how spring security works refer to this URL Spring Security Architecture
I will be making /api/v1/demo
which simply returns string.
controllers/AuthController.java
@RestController
@RequestMapping("/api/v1")
public class AuthController implements ErrorController {
@RequestMapping({"/demo"})
public String demoEndpoint(){
return "Demo";
}
}
security/SecurityConfigurer.java
@Configuration
@EnableWebSecurity
public class SecurityConfigurer extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailService userDetailService;
@Override
public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder.userDetailsService(userDetailService).passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
}
security/services/UserDetailService.java
@Service
public class UserDetailService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
return new User("demo", "demo", new ArrayList<>());
}
}
Now I will introduce JWT(Json Web Token) into project.
JWT consists of three parts separated by (.
),
which are:
- Header
- Payload
- Signature
refer:
The jjwt
dependency helps to create and validate the JWT .
security/jwt/JWTUtil.java
@Component
public class JWTUtil {
private String SECRET_KEY = "MySecretKey";
public String getUsername(String token){
return getClaim(token, Claims::getSubject);
}
public <T> T getClaim(String token, Function<Claims, T> claimResolver) {
final Claims claims = getAllClaims(token);
return claimResolver.apply(claims);
}
private Claims getAllClaims(String token) {
return Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();
}
public Date getExpiration(String token){
return getClaim(token, Claims::getExpiration);
}
private Boolean isTokenExpired(String token){
return getExpiration(token).before(new Date());
}
public String generateToken(UserDetails userDetails){
Map<String, Object> claims = new HashMap<>();
return createToken(claims, userDetails.getUsername());
}
controllers/AuthController.java
@RestController
@RequestMapping("/api/v1")
public class AuthController implements ErrorController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JWTUtil jwtUtil;
@RequestMapping({"/demo"})
public String demoEndpoint(){
return "Demo";
}
@RequestMapping(value = "/authenticate", method = RequestMethod.POST)
public ResponseEntity<?> getAuthToken(@RequestBody AuthRequest authRequest) throws Exception{
try {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
authRequest.getUsername(), authRequest.getPassword()
)
);
}catch (BadCredentialsException e){
throw new Exception("Incorrect username or password", e);
}
final UserDetails userDetails = userDetailsService.loadUserByUsername(authRequest.getUsername());
String jwt = jwtUtil.generateToken(userDetails);
return ResponseEntity.ok(new AuthResponse(jwt));
}
}
As spring putting authentication around every request, we need to tell spring security not to expect authentication for /api/v1/authenticate/
endpoint.
security/SecurityConfigurer.java
@Configuration
@EnableWebSecurity
public class SecurityConfigurer extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailService userDetailService;
@Autowired
private JwtRequestFilter jwtRequestFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests()
.antMatchers("/api/v1/authenticate").permitAll()
.anyRequest().authenticated()
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder.userDetailsService(userDetailService).passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
}
models/AuthRequest.java
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class AuthRequest {
private String username;
private String password;
}
models/AuthResponse.java
@Getter
@AllArgsConstructor
public class AuthResponse {
private final String jwt;
}
To make use of JWT, we need to intercept all incoming requests and get jwt from header. Further by extracting username and password from JWT, we can set username in security context by validating jwt.
security/filters/JwtRequestFilter.java
@Component
public class JwtRequestFilter extends OncePerRequestFilter {
@Autowired
private JWTUtil jwtUtil;
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
final String authorizationHeader = request.getHeader("Authorization");
String jwt_token = null;
String username = null;
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")){
jwt_token = authorizationHeader.substring(7);
username = jwtUtil.getUsername(jwt_token);
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null){
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtUtil.isTokenValid(jwt_token, userDetails)){
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities()
);
usernamePasswordAuthenticationToken.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request)
);
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
}
filterChain.doFilter(request, response);
}
}
Now if I try to hit /api/v1/demo
, response will be 403 Forbidden
I will create a JWT by hitting /api/v1/authenticate
and pass as Authorization header to /api/v1/demo/
Now, I can see the output response.