Xiao Er is a new intern. As a technical leader, I am very responsible. I want to dump everything on him. Again!!!

Xiao Er is a new intern. As the technical leader, I am very responsible and always think of him if there is any good thing. In this case, I arranged a small task of integrating SpringSecurity+JWT to realize login authentication.

I. about SpringSecurity

Before Spring Boot, The use of SpringSecurity was dominated by Shiro, another security management framework, because Shiro was more lightweight to integrate into SSM. The emergence of the Spring Boot has greatly improved this situation. It should be the old saying: one person to heaven, although a little inappropriate, will use it.

This is because Spring Boot provides automatic configuration for SpringSecurity, greatly reducing the learning cost of SpringSecurity. SpringSecurity is also more powerful than Shiro.

Ii. About JWT

JWT is one of the most popular cross-domain authentication solutions: the client initiates a user login request, the server receives and authenticates successfully, generates a JSON object (as shown below), and returns it to the client.

JWT is essentially a Token that generates encrypted user identity information, making it more secure and flexible.

Three, integration steps

First, add a codingmore-security dependency to the module that requires login authentication:

<dependency> <groupId>top.codingmore</groupId> <artifactId>codingmore-security</artifactId> < version > 1.0 - the SNAPSHOT < / version > < / dependency >Copy the code

For example, if the codingmore-admin back-end management module requires login authentication, add the Codingmore-security dependency to the codingmore-admin/pom.xml file.

Second, add the CodingmoreSecurityConfig class to the module that requires login authentication, inheriting from the SecurityConfig class in the Codingmore-Security module.

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class CodingmoreSecurityConfig extends SecurityConfig {
    @Autowired
    private IUsersService usersService;

    @Bean
    public UserDetailsService userDetailsService(a) {
        // Obtain the login user information
        returnusername -> usersService.loadUserByUsername(username); }}Copy the code

The UserDetailsService class is used to load user information, including user name, password, permission, and role set…. One of them is as follows:

UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
Copy the code

In the authentication logic, SpringSecurity calls this method to load the user’s details based on the user name passed in by the client, including determining:

  • Are the passwords consistent?
  • Obtain the rights and roles
    public UserDetails loadUserByUsername(String username) {
        // Query the user based on the user name
        Users admin = getAdminByUsername(username);
        if(admin ! =null) {
            List<Resource> resourceList = getResourceList(admin.getId());
            return new AdminUserDetails(admin,resourceList);
        }
        throw new UsernameNotFoundException("Wrong username or password");
    }
Copy the code

GetAdminByUsername is responsible for querying passwords, roles, permissions, and so on from the database based on the user name.

    public Users getAdminByUsername(String username) {
        QueryWrapper<Users> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("user_login", username);
        List<Users> usersList = baseMapper.selectList(queryWrapper);

        if(usersList ! =null && usersList.size() > 0) {
            return usersList.get(0);
        }

        // The user name is incorrect
        throw new UsernameNotFoundException("User name error");
    }
Copy the code

Step 3: Configure the resource path that does not need security protection in application.yml:

Secure: ignored: urls: # Secure path whitelist - /doc.html - /swagger- UI/** - /swagger/** - /swagger-resources/** - /**/v3/api-docs
      - /**/*.js - /**/*.css
      - /**/*.png - /**/*.ico
      - /webjars/springfox-swagger-ui/** - /actuator/** - /druid/** - /users/login - /users/register - /users/info - /users/logoutCopy the code

Step 4: Add login and refresh token methods to login interface:

@Controller
@api (tags = "user ")
@RequestMapping("/users")
public class UsersController {
    @Autowired
    private IUsersService usersService;
    @Value("${jwt.tokenHeader}")
    private String tokenHeader;
    @Value("${jwt.tokenHead}")
    private String tokenHead;

@apiOperation (value = "return token after login ")
    @RequestMapping(value = "/login", method = RequestMethod.POST)
    @ResponseBody
    public ResultObject login(@Validated UsersLoginParam users, BindingResult result) {
        String token = usersService.login(users.getUserLogin(), users.getUserPass());

        if (token == null) {
            return ResultObject.validateFailed("Wrong username or password");
        }

        // Pass JWT back to the client
        Map<String, String> tokenMap = new HashMap<>();
        tokenMap.put("token", token);
        tokenMap.put("tokenHead", tokenHead);
        return ResultObject.success(tokenMap);
    }

    @apiOperation (value = "refresh token")
    @RequestMapping(value = "/refreshToken", method = RequestMethod.GET)
    @ResponseBody
    public ResultObject refreshToken(HttpServletRequest request) {
        String token = request.getHeader(tokenHeader);
        String refreshToken = usersService.refreshToken(token);
        if (refreshToken == null) {
            return ResultObject.failed("Token has expired!");
        }
        Map<String, String> tokenMap = new HashMap<>();
        tokenMap.put("token", refreshToken);
        tokenMap.put("tokenHead", tokenHead);
        returnResultObject.success(tokenMap); }}Copy the code

Use the Apipost to test this. First, the article fetch interface will prompt you that you are not logged in or that the token has expired if there is no login.

Four, implementation principle

Xiao Er was able to implement login authentication in only four steps, mainly because he encapsulated the code of SpringSecurity+JWT into a common module, let’s look at the directory structure of Codingmore-Security.

Codingmore ws-security ├ ─ ─ component | ├ ─ ─ JwtAuthenticationTokenFilter - JWT login authorization filter | ├ ─ ─ RestAuthenticationEntryPoint | └ ─ ─ RestfulAccessDeniedHandler ├ ─ ─ the config | ├ ─ ─ IgnoreUrlsConfig | └ ─ ─ SecurityConfig └ ─ ─ util └ ─ ─ JwtTokenUtil - JWT's token handling utility classCopy the code

JwtAuthenticationTokenFilter and JwtTokenUtil when speak JWT has talked in detail, simple complement some here.

Client request head carries the token, the server must be needed for each request analytical validation token, so have to define a filter, namely JwtAuthenticationTokenFilter:

  • Get the token from the request header
  • Parse, verify tokens, and verify expiration time
  • Validation is successful, and the validation result is placed in ThreadLocal for the next request

Focus on the other four classes. First RestAuthenticationEntryPoint (custom return results: not login or login expired) :

public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        response.setHeader("Access-Control-Allow-Origin"."*");
        response.setHeader("Cache-Control"."no-cache");
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json"); response.getWriter().println(JSONUtil.parse(ResultObject.unauthorized(authException.getMessage()))); response.getWriter().flush(); }}Copy the code

You can use the debug command to check that the returned message is the error message when the user is not logged in to the article page.

The specific information is defined in the ResultCode class.

public enum ResultCode implements IErrorCode {
    SUCCESS(0."Operation successful"),
    FAILED(500."Operation failed"),
    VALIDATE_FAILED(506."Parameter verification failed"),
    UNAUTHORIZED(401."Not logged in or token has expired"),
    FORBIDDEN(403."No relevant authority.");
    private long code;
    private String message;

    private ResultCode(long code, String message) {
        this.code = code;
        this.message = message; }}Copy the code

Second RestfulAccessDeniedHandler (custom to return the result: there is no access).

public class RestfulAccessDeniedHandler implements AccessDeniedHandler{
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
        response.setHeader("Access-Control-Allow-Origin"."*");
        response.setHeader("Cache-Control"."no-cache");
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json"); response.getWriter().println(JSONUtil.parse(ResultObject.forbidden(e.getMessage()))); response.getWriter().flush(); }}Copy the code

Third IgnoreUrlsConfig (used to configure resource paths that do not require security) :

@Getter
@Setter
@ConfigurationProperties(prefix = "secure.ignored")
public class IgnoreUrlsConfig {
    private List<String> urls = new ArrayList<>();
}
Copy the code

Lombok annotations directly liberalize paths in configuration files that do not require permission verification, such as the Knife4j interface documentation page. If you don’t release it, SpringSecurity intercepts it and you can’t access it.

The fourth SecurityConfig (SpringSecurity common configuration) :

public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired(required = false)
    private DynamicSecurityService dynamicSecurityService;

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity
                .authorizeRequests();

        // Access is allowed for resource paths that do not require protection
        for (String url : ignoreUrlsConfig().getUrls()) {
            registry.antMatchers(url).permitAll();
        }

        // Any request requires authentication
        registry.and()
                .authorizeRequests()
                .anyRequest()
                .authenticated()
                // Disable cross-site request protection and do not use session
                .and()
                .csrf()
                .disable()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                // Custom permission denial handling class
                .and()
                .exceptionHandling()
                .accessDeniedHandler(restfulAccessDeniedHandler())
                .authenticationEntryPoint(restAuthenticationEntryPoint())
                // Custom permission interceptor JWT filter
                .and()
                .addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
        // Add a dynamic permission check filter when dynamic permission is configured
        if(dynamicSecurityService! =null){ registry.and().addFilterBefore(dynamicSecurityFilter(), FilterSecurityInterceptor.class); }}}Copy the code

The main purpose of this class is to tell SpringSecurity which paths do not need to be intercepted, among other things, Login to RestfulAccessDeniedHandler (check), RestAuthenticationEntryPoint (access check) and JwtAuthenticationTokenFilter (JWT filtering).

And add JwtAuthenticationTokenFilter filter to the UsernamePasswordAuthenticationFilter filter before.

Five, the test

The first step is to test the login interface, Apipost direct access to the http://localhost:9002/users/login, you can see the token returned to normal.

The second step, without a token direct access to the interface, can see into the RestAuthenticationEntryPoint the processor:

The third step is to carry token. This time we use Knife4j to test and find that it can be accessed normally:

Source code link:

Github.com/itwanger/co…

This post is on GitHub’s 1.9K + STAR: The Path to Becoming a Java Programmer. Every good Java programmer loves her. The content includes Java foundation, Java concurrent programming, Java virtual machine, Java enterprise development, Java interview and other core knowledge points. Learn Java, look for Java programmers to advance the road 😄.

Github.com/itwanger/to…

Star the repository and you have the potential to become a good Java engineer. Click the link below to jump to the Java Programmer’s Path to Progress website and start your fun learning journey.

tobebetterjavaer.com/

Nothing keeps me here but purpose, and though there are roses by the shore, and trees by the shore, and still harbours, I am not tied.