To prepare
Project GitHub: github.com/Smith-Cruis…
I’ve written two articles about security frameworks before, so you can take a look at them to lay the groundwork.
Shiro+JWT+Spring Boot Restful Tutorial
Spring Boot+Spring Security+Thymeleaf
You need to understand at least the basic configuration of Spring Security and the JWT mechanism before you start.
Some of the Maven configuration and Controller writing here do not say, you can see the source code.
In this project, the JWT key is the user’s own login password, so that each token’s key is different and relatively secure.
Transformation ideas
We usually use Spring Security will use UsernamePasswordAuthenticationFilter and UsernamePasswordAuthenticationToken these two classes, However, these two classes are intended to solve the problem of form login and are not very friendly to Token authentication like JWT. So we’ll develop our own Filter and AuthenticationToken to replace Spring Security’s built-in classes.
The default Spring Security authentication user uses the ProviderManager class. Meanwhile ProviderManager invokes AuthenticationUserDetailsService this interface in populated UserDetails loadUserDetails throws token (T) UsernameNotFoundException to retrieve the user information from the database, this method requires the user to inherit implementation). Because of considering the own way is not very good support JWT, such as UsernamePasswordAuthenticationToken assignment by have a username and password in the field, but JWT is attached in the header of the request, There is no such thing as username and password when there is only one token.
So I have to, for example, the user’s method is not realized in AuthenticationUserDetailsService, but this may not be able to perfect follow Spring Security official design, please correct me if there is a better way.
transform
transformAuthentication
Authentication is an interface that is officially provided by the Security and is the core of the call Authentication stored in SecurityContextHolder.
Here are three methods
GetCredentials () was originally used to get the password, but now we’re going to use it to store the tokens passed by the front end
GetPrincipal () was originally used to hold user information, but we’ll keep it. For example, some key information such as user username, ID and so on is stored in the Controller
GetDetails () returns some miscellaneous details, such as the client IP, but since this is basically a restful request, this is not important, so it is emasculated :happy:
The Authentication interface is provided by default
public interface Authentication extends Principal.Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
Object getCredentials(a);
Object getDetails(a);
Object getPrincipal(a);
boolean isAuthenticated(a);
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
Copy the code
JWTAuthenticationToken
Let’s write our own Authentication, noting the difference between the two constructors. A class of AbstractAuthenticationToken is official Authentication.
public class JWTAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private final Object principal;
private final Object credentials;
/** * setAuthenticated(false) *@paramToken JWT key */
public JWTAuthenticationToken(String token) {
super(null);
this.principal = null;
this.credentials = token;
setAuthenticated(false);
}
JWTAuthenticationToken = JWTAuthenticationToken = JWTAuthenticationToken = JWTAuthenticationToken * setAuthenticated(true) * because it has been identified successfully@paramToken JWT key *@paramUserInfo User information, such as username, ID, and so on *@param*/ the authority that has been granted to the authorities
public JWTAuthenticationToken(String token, Object userInfo, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = userInfo;
this.credentials = token;
setAuthenticated(true);
}
@Override
public Object getCredentials(a) {
return credentials;
}
@Override
public Object getPrincipal(a) {
returnprincipal; }}Copy the code
Transform the AuthenticationManager
This parameter is used to determine whether the user token is valid
JWTAuthenticationManager
@Component
public class JWTAuthenticationManager implements AuthenticationManager {
@Autowired
private UserService userService;
/** * Perform token authentication *@paramAuthentication JWTAuthenticationToken * to be authenticated@returnThe authenticated JWTAuthenticationToken is used by the Controller@throwsAuthenticationException throws */ if authentication fails
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String token = authentication.getCredentials().toString();
String username = JWTUtil.getUsername(token);
UserEntity userEntity = userService.getUser(username);
if (userEntity == null) {
throw new UsernameNotFoundException("This user does not exist");
}
/* * The official recommendation is that three exceptions must be handled in this method, BadCredentialsException = BadCredentialsException = DisabledException = BadCredentialsException You can customize this to suit your business needs * see the AuthenticationManager JavaDoc */ for details
boolean isAuthenticatedSuccess = JWTUtil.verify(token, username, userEntity.getPassword());
if (! isAuthenticatedSuccess) {
throw new BadCredentialsException("Incorrect user name or password");
}
JWTAuthenticationToken authenticatedAuth = new JWTAuthenticationToken(
token, userEntity, AuthorityUtils.commaSeparatedStringToAuthorityList(userEntity.getRole())
);
returnauthenticatedAuth; }}Copy the code
Develop your own Filter
Then we will use our own filters, considering that token is added in the header, this like those BasicAuthentication certification, so we inherit BasicAuthenticationFilter rewrite the core method reform.
JWTAuthenticationFilter
public class JWTAuthenticationFilter extends BasicAuthenticationFilter {
/** * Use our own JWTAuthenticationManager@paramAuthenticationManager Our own developed JWTAuthenticationManager */
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String header = request.getHeader("Authorization");
if (header == null| |! header.toLowerCase().startsWith("bearer ")) {
chain.doFilter(request, response);
return;
}
try {
String token = header.split("") [1];
JWTAuthenticationToken JWToken = new JWTAuthenticationToken(token);
If authentication fails, the AuthenticationManager will throw an exception that we catch
Authentication authResult = getAuthenticationManager().authenticate(JWToken);
// Write the authenticated Authentication to SecurityContextHolder for later use
SecurityContextHolder.getContext().setAuthentication(authResult);
} catch (AuthenticationException failed) {
SecurityContextHolder.clearContext();
// Authentication failed
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, failed.getMessage());
return; } chain.doFilter(request, response); }}Copy the code
configuration
SecurityConfig
// Enable method annotation
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JWTAuthenticationManager jwtAuthenticationManager;
@Override
protected void configure(HttpSecurity http) throws Exception {
// restful This function is disabled because it is innate to defend against CSRF attacks
http.csrf().disable()
// By default, all requests are allowed through, and then we use method annotations to granular control permissions
.authorizeRequests().anyRequest().permitAll()
.and()
/ / to add our own filters, note because we don't have open formLogin (), so UsernamePasswordAuthenticationFilter not be invoked
.addFilterAt(new JWTAuthenticationFilter(jwtAuthenticationManager), UsernamePasswordAuthenticationFilter.class)
// The front-end separation is inherently stateless, so we don't need cookies and sessions. All information is stored in a token..sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); }}Copy the code
There are a lot of tricks for method annotation authentication. Check out this simple tutorial on Spring Boot+Spring Security+Thymeleaf
Uniform Global Exception
A restful final exception throw must be in a uniform format, so as to facilitate the front end of the call.
We normally use RestControllerAdvice to unify exceptions, but it can only manage our own exceptions, not the framework’s own exceptions, such as 404, so we need to modify the ErrorController
ExceptionController
@RestControllerAdvice
public class ExceptionController {
// Catch all exceptions thrown by the controller itself
@ExceptionHandler(Exception.class)
public ResponseEntity<ResponseBean> globalException(Exception ex) {
return new ResponseEntity<>(
new ResponseBean(
HttpStatus.INTERNAL_SERVER_ERROR.value(), ex.getMessage(), null), HttpStatus.INTERNAL_SERVER_ERROR ); }}Copy the code
CustomErrorController
If you want to implement the ErrorController interface directly, there are many existing methods that do not work well, so we choose AbstractErrorController
@RestController
public class CustomErrorController extends AbstractErrorController {
// Exception path url
private final String PATH = "/error";
public CustomErrorController(ErrorAttributes errorAttributes) {
super(errorAttributes);
}
@RequestMapping("/error")
public ResponseEntity<ResponseBean> error(HttpServletRequest request) {
// Obtain the exception information in the request, there are many, such as time, path, you can traverse the map to see
Map<String, Object> attributes = getErrorAttributes(request, true);
// Select only the return message field
return new ResponseEntity<>(
new ResponseBean(
getStatus(request).value() , (String) attributes.get("message"), null), getStatus(request)
);
}
@Override
public String getErrorPath(a) {
returnPATH; }}Copy the code
test
Write a controller, and you can also use my controller to get user information. I recommend using @authenticationPrincipal!
@RestController
public class MainController {
@Autowired
private UserService userService;
// Log in to obtain the token
@PostMapping("login")
public ResponseEntity<ResponseBean> login(@RequestParam String username, @RequestParam String password) {
UserEntity userEntity = userService.getUser(username);
if (userEntity==null| |! userEntity.getPassword().equals(password)) {return new ResponseEntity<>(new ResponseBean(HttpStatus.BAD_REQUEST.value(), "login fail".null), HttpStatus.BAD_REQUEST);
}
/ / JWT's signature
String token = JWTUtil.sign(username, password);
return new ResponseEntity<>(new ResponseBean(HttpStatus.OK.value(), "login success", token), HttpStatus.OK);
}
// Anyone can access the method to determine whether the user is valid
@GetMapping("everyone")
public ResponseEntity<ResponseBean> everyone(a) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication.isAuthenticated()) {
// Login user
return new ResponseEntity<>(new ResponseBean(HttpStatus.OK.value(), "You are already login", authentication.getPrincipal()), HttpStatus.OK);
} else {
return new ResponseEntity<>(new ResponseBean(HttpStatus.OK.value(), "You are anonymous".null), HttpStatus.OK); }}@GetMapping("user")
@PreAuthorize("hasAuthority('ROLE_USER')")
public ResponseEntity<ResponseBean> user(@AuthenticationPrincipal UserEntity userEntity) {
return new ResponseEntity<>(new ResponseBean(HttpStatus.OK.value(), "You are user", userEntity), HttpStatus.OK);
}
@GetMapping("admin")
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
public ResponseEntity<ResponseBean> admin(@AuthenticationPrincipal UserEntity userEntity) {
return new ResponseEntity<>(new ResponseBean(HttpStatus.OK.value(), "You are admin", userEntity), HttpStatus.OK); }}Copy the code
other
Here are some quick answers to common questions.
Verifying whether the Token is valid is too resource-intensive per request to the database
It is impossible for us to get data from the database every time to judge whether the token is legitimate, which is a waste of resources and affects efficiency.
We can use the cache in JWTAuthenticationManager.
When the user accesses the database for the first time, we query the database to determine whether the token is valid. If the token is valid, we put it into the cache (the expiration time of the cache is the same as the expiration time of the token). After that, each request first searches in the cache.
How to solve the JWT expiration problem
Write a method in JWTAuthenticationManager that raises a specific exception, such as ReAuthenticateException, when the token is about to expire, Then we catch the exception separately in JWTAuthenticationFilter, return a specific HTTP status code, and then the front end separately access GET /re_authentication to GET a new token to replace the original one. The old token is also removed from the cache.