The framework of this paper mainly refers to github.com/Snailclimb/… This paper only analyzes some of the key codes and problems
The project requirements
- Spring Boot version 2.0.3;
- Spring Security version 5.1.6;
- Mybatis version 1.3.2
- Use the plug-in Lombook
Results show
Use Postman to display the effects. Some of these effects need to be implemented in conjunction with filter exception catching, which I’ll cover in my next article.
- If the login is correct, the value of the returned header whose key is Authorization is the token.
- Account error
- Password mistake
- The header contains the correct token for access
- Carrying incorrect or expired tokens
- Insufficient access
1. Create the User entity User and the permission entity UserRole
@Entity
@Data
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String username;
private String password;
}
Copy the code
@Data
@Entity
public class UserRole {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String role;
}
Copy the code
Create the corresponding data table in the database. Build Mapper, query statement is
@Select("select * from user_role where username = #{username}")
List<UserRole> getUserRoleByUserName(String username);
Copy the code
2. Create a login entity
This entity is only used for login functions
@Data
public class LoginRequest {
private String username;
private String password;
}
Copy the code
3. Implement populated UserDetails
This is a mandatory interface in Spring Security.
private Collection<? extends GrantedAuthority> authorities;
public JwtUser(User user) {
this.id = user.getId();
this.username = user.getUsername();
this.password = user.getPassword();
this.authorities = CurrentUserUtils.getCurrentUserRoles(this.username);
}
Copy the code
GetCurrentUserRoles is a static method in the utility class to get the permissions of the currently logged in user.
public static List<SimpleGrantedAuthority> getCurrentUserRoles(String username) { List<UserRole> userRoles = currentUserUtils.userService.getUserRolesByUsername(username); Mapper List<SimpleGrantedAuthority> authorities = new ArrayList<>(); userRoles.forEach(userRole -> authorities.add(new SimpleGrantedAuthority("ROLE_" + userRole.getRole())));
return authorities;
}
Copy the code
Note that the following code is required to inject beans into the utility class, otherwise Spring will not be able to find the appropriate Bean injection using @Autrowired
@PostConstruct
public void init() {
currentUserUtils = this;
currentUserUtils.userService = this.userService;
}
Copy the code
4. Implement UserDeatilsService
This is the interface that must be implemented in Spring Security, where the loadUserByUsername() function must be implemented to correctly find the corresponding user by user name. This function will be thrown UsernameNotFountException. Note, however, that Spring Security by default converts this exception to a BadCredentialsException, which you can configure in configuration to throw correctly, as opposed to a password error, as described below in configuration.
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userService.findUserByUsername(username);
if(user==null) {
throw new UsernameNotFoundException("User name does not exist!");
}
return new JwtUser(user);
}
Copy the code
5. Token Creates a tool class
The utility class jwTtokenUtils.java is used to generate tokens through io.jsonWebToken.jwts. The specific code is as follows
private static final byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(SecurityConstants.JWT_SECRET_KEY);
//JWT_SECRET_KEY = "C*F-JaNdRgUkXn2r5u8x/A? D(G+KbPeShVmYq3s6v9y$B&E)H@McQfTjWnZr4u7w"; Private static final Key secretKey = keys. hmacShaKeyFor(apiKeySecretBytes); public static String createToken(String username, List<String> roles) { final Date createdDate = new Date(); final Date expirationDate = new Date(createdDate.getTime() + SecurityConstants.EXPIRATION * 1000); //EXPIRATION = 60 * 60L; String tokenPrefix = jwts.Builder ().setheaderParam ()"type", SecurityConstants.TOKEN_TYPE)
//TOKEN_HEADER = "Authorization"; SignWith (secretKey, SignatuRealGorithM.hs256) // Encryption algorithm.claim (SecurityConstants.ROLE_CLAIMS, String.join(",", roles))
//ROLE_CLAIMS = "rol"; Encrypt permissions. SetIssuer ("Ddm"SetIssuedAt (createdDate).setSubject(username).setexpiration (expirationDate).compact();return SecurityConstants.TOKEN_PREFIX + tokenPrefix;
}
Copy the code
6. User-defined filter implementation
1. Authorization filter
Authorization is when a user logs in, authenticates his login information, creates a token for him and returns it in the header if it is correct.
Custom authorization filters need to implement UsernamePasswordAuthenticationFilter (), Mainly is to achieve attemptAuthentication (), successfulAuthentication (), unsuccessfullAuthentication () the three functions.
The purpose of implementing a custom authorization filter is as follows:
-
- You can use customized login information to verify the username and password. If information other than username and password exists or the username and password fields are modified, the username and password cannot be obtained correctly. In practice, this is unlikely to happen, so the function can optionally not be overwritten.
-
- UsernamePasswordAuthenticationFilter no successfulAuthentication () () the default implementation, we need to implement a user name password authentication authorization after the completion of work. The token is created using a custom token creation tool (createToken() in JwtTokenUtils), which corresponds to subsequent token resolution.
-
- Simple implementation unsuccessfulAuthentication (), if verification is a user name and password mistake, will enter into the function.
@Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { ObjectMapper objectMapper = new ObjectMapper(); Try {// Get the login information from request.getParameter. LoginRequest LoginRequest = objectMapper.readValue(request.getinputStream (), loginRequest.class); / / in this section and attemptAuthentication method is the same source UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( loginRequest.getUsername(), loginRequest.getPassword());returnauthenticationManager.authenticate(authentication); } catch (IOException e) {e.printStackTrace();returnnull; }} /** * If the validation succeeds, */ @override protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, org.springframework.security.core.Authentication authentication) { JwtUser jwtUser = (JwtUser) authentication.getPrincipal(); List<String> authorities = jwtUser.getAuthorities() .stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.toList()); / / create a Token String Token = JwtTokenUtils. CreateToken (jwtUser. GetUsername (), authorities); // Return Token Response. setHeader(SecurityConstants.TOKEN_HEADER, Token) in Http Response Header; } @Override protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException authenticationException) throws IOException { response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authenticationException.getMessage()); }Copy the code
2. Authentication filter
The user has been authorized, but if he wants to access our other services, he must carry the token generated for him in the header. This step of token verification is called authentication. Only authenticated users can access the corresponding services correctly.
The custom authentication filter is mainly to rewrite the function doFilterInternal to get the corresponding information through our custom JwtTokenUtils, otherwise the default method will not get the correct information.
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws IOException, ServletException {
String token = request.getHeader(SecurityConstants.TOKEN_HEADER);
if(token == null || ! token.startsWith(SecurityConstants.TOKEN_PREFIX)) { SecurityContextHolder.clearContext(); }else{ UsernamePasswordAuthenticationToken authentication = getAuthentication(token); SecurityContextHolder.getContext().setAuthentication(authentication); } chain.doFilter(request, response); } / retrieve the user Authentication information Authentication * * * * / private UsernamePasswordAuthenticationToken getAuthentication (String authorization) { String token = authorization.replace(SecurityConstants.TOKEN_PREFIX,"");
try {
String username = JwtTokenUtils.getUsername(token);
logger.info("checking username:" + username);
if(! Stringutils. isEmpty(username)) {// If the user's role has been changed, the user's role has been changed userDetailsService.loadUserByUsername(username); UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, null, userDetails.getAuthorities());return userDetails.isEnabled() ? usernamePasswordAuthenticationToken : null;
}
} catch (ExpiredJwtException | MalformedJwtException | IllegalArgumentException exception) {
logger.warning("Request to parse JWT with invalid signature . Detail : " + exception.getMessage());
}
return null;
}
Copy the code
7. Exception handling
1. The token is incorrect or expired
The exception information is resolved by implementing the AuthenticationEntryPoint() function.
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { /** * When a user attempts to access a REST resource that requires permission and does not provide a Token or the Token is incorrect or expired, * this method will be invoked to send a 401 response along with an error message */ @override public void denote (HttpServletRequest Request, HttpServletResponse Response, AuthenticationException authException) throws IOException { response.sendError(HttpServletResponse.SC_UNAUTHORIZED, SecurityConstants.TOKEN_ILLEGAL); }}Copy the code
2. The access permission is insufficient
For example, if a resource must have ADMIN permission to access it, USER permission access will be blocked. Implement AcessDeniedHandler() to handle this problem.
Public class JwtAccessDeniedHandler implements AccessDeniedHandler { Override public void Handle (HttpServletRequest Request, HttpServletResponse Response, AccessDeniedException accessDeniedException) throws IOException { response.sendError(HttpServletResponse.SC_FORBIDDEN, SecurityConstants.AUTHORITY_DENY); }}Copy the code
8. General configuration
- The userDetailsServiceImpl you just defined, the two exception handlers, and the BCryptPasswordEncoder (a password encryptor that will not see the correct password in the database after use) are injected or initialized as @Beans. Here, we can through setHideUserNotFountExceptions (false) for the spring to return UsernameNotFoundException correctly.
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setHideUserNotFoundExceptions(false); / / error is used to distinguish the user name and password error provider. SetPasswordEncoder (bCryptPasswordEncoder ()); provider.setUserDetailsService(userDetailsServiceImpl);returnprovider; } @ Override protected void the configure (AuthenticationManagerBuilder auth) {/ / set custom userDetailsService and password encoder auth.authenticationProvider(authenticationProvider()); }Copy the code
- Rewrite the configure (AuthenticationManagerBuilder auth) and configure (HttpSecurity HTTP).
@override protected void configure(HttpSecurity HTTP) throws Exception {http.cors(). And () // Disable csrf.csrf ().disable() .authorizeRequests() .antMatchers(HttpMethod.POST,"/auth/*".antmatchers (httpmethod.get,).permitall () // Specify a path for resources that require authenticated users to access."/api/**").hasRole("USER")
.antMatchers(HttpMethod.DELETE, "/api/**").hasRole("ADMIN"AnyRequest ().permitall (). And () // Add custom filter.addfilter (new) JwtAuthenticationFilter(authenticationManager())) .addFilter(new JwtAuthorizationFilter(authenticationManager(), UserDetailsServiceImpl)) // No session required (no session created) SessionManagement (.) sessionCreationPolicy (sessionCreationPolicy. STATELESS). And () / / authorization exception handling .exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint()) .accessDeniedHandler(jwtAccessDeniedHandler()); Http.headers ().frameoptions ().disable(); }Copy the code
At this point, Spring Security is fully configured and can implement token authorization and authentication.