background

This series of tutorials was prepared as an internal training resource for the team. We mainly experience various features of SpringSecurity in an experimental way.

Session management is a big topic, so the well-known cookie-session mode will be ignored. Today, I will focus on stateless sessions: Token-based JWT (JSON Web Token), which is a Session management mode suitable for micro-service architecture. Session sharing, OAuth2.0 and other distributed cluster Session management will be involved later.

JWT profile

Introduction to JWT, there are many online resources: Jwt. IO /introductio… JWT consists of three parts: Header, Payload, and Signature, with dots between them. Delimited, the first two parts use Base64 encoding (for more on Base64 encoding) as follows:

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)
Copy the code

Stateless versus stateful

  • Stateful service

Stateful service means that the server records the client information of each Session, so as to identify the client identity and conduct corresponding processing according to the user identity. HTTP itself is stateless and short-connected, thus giving rise to our traditional cookie-session mode, which is widely used in single architecture. After a user logs in, the Session information between the user and the user is stored in the Session of the server, and then the server responds with a SessionID to the front end, which stores the SessionID in the Cookie. Subsequent requests continue to initiate requests with the Cookie information, and the back end queries the corresponding Session information. Complete the request response.

This approach presents some problems in microservices architecture:

  1. After a session is established, the server needs to store the session information, which increases the storage and query pressure on the server and occupies precious storage and computing resources.
  2. The server saves user status, which is difficult to be horizontally extended. State replication and synchronization (Session synchronization and Session sharing) must be carried out on each server before expansion.
  • Stateless service

It is easy to understand stateful service and stateless service. In practice, the more common implementation of stateless service is token-based, that is:

  1. The server does not save any client session information.
  2. Each request from the client must carry a token, which contains the authenticator and signature information (user name, role, permission, etc.).

Advantages and disadvantages of JWT

  • Using JWT for authentication processing has the following advantages:

    • JWT is token-based, disperses the user state to the client side, and the server side is stateless, reducing the pressure on the server and improving the performance;
    • JWT is strictly structured and contains relevant messages about authenticated users. Once the verification is successful, the resource server does not need to verify the validity of the information to the authentication server.
    • The payload in JWT can be customized, so developers can expand the definition based on service needs, such as adding information such as whether the user is an administrator and the bucket where the user belongs to meet service needs.
    • JWT is small in size, easy to transport, and supports the transmission of URL/POST parameters or HTTP headers, so it can support a variety of clients, not only the Web.
    • JWT uses JSON format and has excellent cross-language support;
    • JWT supports cross-domain, making single sign-on development easier.
  • The following points need to be carefully considered when implementing a JWT security solution:

    • JWT token logout: Because JWT token is stored on the client, when the user logs out, the client may store it before the validity period expires. At this time, developers need to effectively prevent the access to the token after logout. Developers can use API gateway to achieve this. In addition, using short-term tokens is also a good solution.
    • JWT token length: Because JWT allows developers to customize token extensions, too much information in the JWT payload can cause the client to lengthen each request header, affecting the request speed.
    • Avoid new system bottlenecks: The API gateway service accesses and authenticates the authentication server, which may cause new system bottlenecks.
    • Effective defense against XSS attacks: Since JWT is stored on the client, it is most likely to trigger XSS attacks. Therefore, effective defense must be taken when using JWT.

The same rules, still use the experimental way to test, but this time to see the effect:

Experiment 0: Log in to get JWT

Experiment 1: Carrying JWT: normal response

Experiment 2: No JWT: No authenticated response

Experiment 3: Carrying JWT: no permission response

Experiment 4: Carrying JWT: Expired response

Experiment 5: Carrying JWT: illegal format response

coded

Create a new SpringBoot project and name it springboot-security-jwt. The core dependencies are Web, SpringSecurity and JJWT.

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>0.7.0</version>
    </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>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>
Copy the code

Create resource interfaces: /user/add, /user/query, and the default home path/to display information about the logged in user or anonymous user if not logged in.

@RestController
@Slf4j
public class HelloController {
    @GetMapping(value = "/user/add")
    public String accessResource1(a) {
        return " Access Resource 1: Add User";
    }

    @GetMapping(value = "/user/query")
    public String accessResource2(a) {
        return " Access Resource 2: Query User";
    }

    @GetMapping(value = "/")
    public String index(a) {
        log.info(SecurityContextHolder.getContext().getAuthentication().toString());
        return "Welcome "+ SecurityContextHolder.getContext().getAuthentication(); }}Copy the code
  • Create two users in memory:

    • The dev user has the dev and test roles.
    • The test user has only the test role.
  • Configuring resource authorization:

    • /user/add requires a dev role to access;
    • /user/query can be accessed only with the test role;

Security configuration classes:

@Configuration
@Slf4j
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    private final JwtAccessDeniedHandler jwtAccessDeniedHandler;
    private final JwtAuthenticationEntryPoint authenticationErrorHandler;

    public SecurityConfig(JwtAccessDeniedHandler jwtAccessDeniedHandler, JwtAuthenticationEntryPoint authenticationErrorHandler) {
        this.jwtAccessDeniedHandler = jwtAccessDeniedHandler;
        this.authenticationErrorHandler = authenticationErrorHandler;
    }

    @Bean
    PasswordEncoder passwordEncoder(a) {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // There is no PasswordEncoder mapped for the id "null"
        PasswordEncoder encoder = passwordEncoder();

        String yourPassword = "123";
        log.info("Encoded password: " + encoder.encode(yourPassword));

        // Config account info and permissions
        auth.inMemoryAuthentication()
                .withUser("dev").password(encoder.encode(yourPassword)).roles("dev"."test")
                .and()
                .withUser("test").password(encoder.encode(yourPassword)).authorities("ROLE_test");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/user/add").hasRole("dev")
                .antMatchers("/user/query").hasAuthority("ROLE_test")
                .antMatchers("/user/**").authenticated()
                .anyRequest().permitAll() // Let other request pass
                .and()
                .csrf().disable() // turn off csrf, or will be 403 forbidden
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // stateless
                .and()
                .formLogin()
                .successHandler(new AuthenticationSuccessHandler() {
                    @Override
                    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException {
                        log.info("Login Successfully");
                        SecurityContextHolder.getContext().setAuthentication(authentication);
                        String token = JwtUtil.createToken(authentication);
                        httpServletResponse.getWriter().write(token);
                    }
                })
                .failureHandler(new AuthenticationFailureHandler() {
                    @Override
                    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException {
                        log.info("Login Error");
                        httpServletResponse.getWriter().write(e.getLocalizedMessage());
                    }
                })
                .and()
                .addFilterBefore(newJwtAuthenticationFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class) .exceptionHandling() .authenticationEntryPoint(authenticationErrorHandler) .accessDeniedHandler(jwtAccessDeniedHandler); }}Copy the code

JWT filter class: JwtAuthenticationFilter

public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
    public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        // Extract the token from the ab initio information
        String token = JwtUtil.resolveToken(request);
        if(token ! =null) {
            // Resolve the token using the JWT tool method
            Authentication authentication = JwtUtil.getAuthentication(token);
            // Set the authentication information to the context, note the stateless setting!SecurityContextHolder.getContext().setAuthentication(authentication); } chain.doFilter(request, response); }}Copy the code

Unauthorized, unauthorized interception: JwtAuthenticationEntryPoint, JwtAccessDeniedHandler

@Component
@Slf4j
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException {
        // This method is called to send a 401 response when a user attempts to access a secure REST resource without providing any credentials
        log.info("UNAUTHORIZED"); response.sendError(HttpServletResponse.SC_UNAUTHORIZED, e.getMessage()); }}@Component
@Slf4j
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException {
        // When a user accesses a protected REST resource without authorization, this method is called to send the 403 Forbidden response
        log.info("FORBIDDEN"); response.sendError(HttpServletResponse.SC_FORBIDDEN, e.getMessage()); }}Copy the code

JWT utility classes and constant classes: JwtUtil, JwtConstant

public class JwtUtil {
    /** * Generate JWT token **@param authentication
     * @return* /
    public static String createToken(Authentication authentication) {
        String authorities = authentication.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.joining(","));

        long now = (new Date()).getTime();
        Date validity = new Date(now + JwtConstant.VALIDITY_SECONDS * 1000);

        return Jwts.builder()
                .setSubject(authentication.getName())
                .claim(JwtConstant.AUTH_KEY, authorities)
                .signWith(SignatureAlgorithm.HS512, JwtConstant.SECRET)
                .setExpiration(validity)
                .compact();
    }

    /** * Decrypt JWT token **@param token
     * @return* /
    public static Authentication getAuthentication(String token) {
        Claims claims = Jwts.parser()
                .setSigningKey(JwtConstant.SECRET)
                .parseClaimsJws(token)
                .getBody();

        Collection<? extends GrantedAuthority> authorities =
                Arrays.stream(claims.get(JwtConstant.AUTH_KEY).toString().split(","))
                        .map(SimpleGrantedAuthority::new)
                        .collect(Collectors.toList());

        User principal = new User(claims.getSubject(), "", authorities);
        return new UsernamePasswordAuthenticationToken(principal, token, authorities);
    }

    /** * Resolve token ** from request header@param request
     * @return* /
    public static String resolveToken(HttpServletRequest request) {
        String bearerToken = request.getHeader(JwtConstant.HEADER);
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(JwtConstant.TOKEN_PREFIX)) {
            return bearerToken.substring(7);
        }
        return null; }}public final class JwtConstant {
    /** * Symmetric encryption key * Only servers are stored. In production, you are advised to use high complexity keys or use asymmetric encryption eg:RSA */
    public static final String SECRET = "heartsuit";

    /** * Token validity period */
    public static final long VALIDITY_SECONDS = 60 * 60 * 12; // default 12 hours

    /** * permission */
    public static final String AUTH_KEY = "auth";

    /** * the Token Key */ in the header
    public static final String HEADER = "authorization";

    /** * Token prefix */
    public static final String TOKEN_PREFIX = "Bearer ";

    private JwtConstant(a) {}}Copy the code

How far away from production

  1. FormLogin, for demonstration purposes, is a little tricky, but you can’t log in from a browser form anymore, and real projects these days tend to be separated from the front end. Here, the JWT generated after a successful login is placed directly in successHandler for formLogin for demonstration purposes only.

  2. In memory mode, user information (user name, password, and permission) is stored in simple memory mode. The actual production should use the database, convenient expansion;

  3. Permission hard coding, in security configuration, permission interception by manual writing; The actual production should be dynamically configured after querying from the database.

  4. Configure constants, using a constant class for JWT configuration information; In practice, it can be written to the configuration file (or configuration center) and read by SpringBoot configuration properties.

  5. Symmetric encryption. Here, symmetric encryption is used for signature and verification of JWT. It is suggested to use asymmetric encryption algorithm eg: RSA in actual production.

  6. Exception interception. Exceptions with expired JWT and incorrect format are automatically thrown by JWT dependent packages. Interception and further encapsulation should be carried out in actual production to optimize interface call experience.

If each of the above points is 10%, then more than half of the work is still to be done before production, so the previous experiments or code is just on paper, just for demonstration purposes, meaning is enough.

Matters needing attention

  1. CSRF protection must be disabled

.csrf().disable()

  1. Be sure to use stateless

.sessionCreationPolicy(SessionCreationPolicy.STATELESS)

Source Code

Github

Reference

  • SpringSecurity official documentation
  • JWT profile
  • JWT debugging

If you have any questions or any bugs are found, please feel free to contact me.

Your comments and suggestions are welcome!

This article has participated in the activity of “New person creation Ceremony”, and started the road of digging gold creation together.