Authentication and authorization are more or less involved in most projects. This project uses JWT and Spring Security to do it. This tutorial focuses on implementation and will not go into too much depth on these two technologies.

Add the following to the pom.xml dependency configuration:

<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt</artifactId>
  <version>0.9.0</version>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
  <groupId>jakarta.xml.bind</groupId>
  <artifactId>jakarta.xml.bind-api</artifactId>
</dependency>
Copy the code

JWT configuration

First we need to add some JWT configuration to the application.yml file:

# application.yml
jwt:
  issue: wxbox
  token-header: Authorization
  token-prefix: 'Bearer '
  expiration: 604800

# application-dev.yml
jwt:
  secret: 1048c08c3a502d78feex2b59ce243342

# application-prod.yml
jwt:
  secret: 1048c08c3a502d78feex2b59ce243342
Copy the code

Then create a JWT utility class:

package com.foxescap.wxbox.common;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.Date;
import java.util.function.Function;

/ * * *@author xfly
 */
@Data
@ConfigurationProperties(prefix = "jwt")
@Component
public class JwtUtil {

    private String tokenHeader;

    private String tokenPrefix;

    private String issuer;

    private String secret;

    private Long expiration;

    /** * Create Token *@paramUserDetails User information *@return token
     */
    public String createToken(UserDetails userDetails) {
        final Date issuedAt = new Date();

        var roles = new ArrayList<String>();
        for (var role : userDetails.getAuthorities()) {
            roles.add(role.getAuthority());
        }

        return Jwts.builder()
                .setHeaderParam("typ"."JWT")
                .signWith(SignatureAlgorithm.HS256, secret)
                .claim("rol", String.join(",", roles))
                .setIssuer(issuer)
                .setIssuedAt(issuedAt)
                .setSubject(userDetails.getUsername())
                .setExpiration(new Date(issuedAt.getTime() + expiration * 1000))
                .compact();
    }

    /** * Check whether Token expires *@param token token
     * @returnTrue - Expired false- Not expired */
    public boolean isTokenExpired(String token) {
        final Date expiration = getExpirationFromToken(token);

        return expiration.before(new Date());
    }

    /** * Check whether the Token is valid *@param token token
     * @paramUserDetails User information *@returnTrue - legal false- Illegal */
    public Boolean validateToken(String token, UserDetails userDetails) {
        final String username = getUsernameFromToken(token);

        return(username.equals(userDetails.getUsername())) && ! isTokenExpired(token); }/** * Obtain the user name from the Token *@param token token
     * @returnUser name * /
    public String getUsernameFromToken(String token) {
        return getClaimFromToken(token, Claims::getSubject);
    }

    /** * Get expiration time from Token *@param token token
     * @returnExpiration time */
    public Date getExpirationFromToken(String token) {
        return getClaimFromToken(token, Claims::getExpiration);
    }

    /** * Split the Token and get the required part *@param token token
     * @paramHow to get the required part of claimsResolver *@param <T> T
     * @returnRequired parts */
    private <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {

        Claims claims = Jwts.parser()
                .setSigningKey(secret)
                .parseClaimsJws(token)
                .getBody();

        returnclaimsResolver.apply(claims); }}Copy the code

We automatically populate the above configuration information with the corresponding properties via the @ConfigurationProperties(prefix = “JWT “) annotation.

Implement the UserDetails interface

In general, we need to implement the UserDetails interface to customize some logic:

package com.foxescap.wxbox.model;

import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/ * * *@author xfly
 */
@Data
public class Admin implements UserDetails {

    @TableId(type = IdType.AUTO)
    private Long id;

    private String username;

    private String password;

    private String role;

    private String regIp;

    private String loginIp;

    private LocalDateTime loginAt;

    private Integer status;

    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createdAt;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<SimpleGrantedAuthority> authorities = new ArrayList<>();
        authorities.add(new SimpleGrantedAuthority("ROLE_" + role.toUpperCase()));

        return authorities;
    }

    @Override
    public boolean isAccountNonExpired(a) {
        return true;
    }

    @Override
    public boolean isAccountNonLocked(a) {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired(a) {
        return true;
    }

    @Override
    public boolean isEnabled(a) {
        return status == 1; }}Copy the code

Note that we use Lombok’s @data annotation, and if that doesn’t work you’ll need to override the getUsername() and getPassword() methods.

Implement the UserDetailService interface

This interface has only one abstract method: loadUserByUsername(), which is implemented in our AdminService:

@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
    var admin = lambdaQuery().eq(Admin::getUsername, s).one();
    if(admin ! =null) {
        return admin;
    }
    throw new UsernameNotFoundException("User not found with username: " + s);
}
Copy the code

Implementing authentication filters

package com.foxescap.wxbox.filter;

import com.foxescap.wxbox.common.ApiResponse;
import com.foxescap.wxbox.common.JwtUtil;
import com.foxescap.wxbox.service.AdminService;
import io.jsonwebtoken.JwtException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/ * * *@author xfly
 */
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;

    private final AdminService adminService;

    public JwtAuthenticationFilter(JwtUtil jwtUtil, AdminService adminService) {
        this.jwtUtil = jwtUtil;
        this.adminService = adminService;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        String authTokenHeader = request.getHeader(jwtUtil.getTokenHeader());
        String token;
        String username;

        if (authTokenHeader == null| |! authTokenHeader.startsWith(jwtUtil.getTokenPrefix())) { SecurityContextHolder.clearContext(); }else {
            token = authTokenHeader.replaceAll(jwtUtil.getTokenPrefix(), "");
            try {
                username = jwtUtil.getUsernameFromToken(token);
                UserDetails userDetails = adminService.loadUserByUsername(username);
                if (jwtUtil.validateToken(token, userDetails)) {
                    UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                    usernamePasswordAuthenticationToken.setDetails(newWebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken); }}catch (JwtException e) {
                ApiResponse.fail(response, e.getMessage());
                return; } } chain.doFilter(request, response); }}Copy the code

The apiResponse.fail method is as follows:

/** * returns * on failure@param response HttpServletResponse
 * @paramMSG information *@throws IOException IOException
 */
public static void fail(HttpServletResponse response, String msg) throws IOException {
    response.setContentType("application/json; charset=utf-8");
    response.setCharacterEncoding("UTF-8");
    var out = response.getOutputStream();
    out.write(new ObjectMapper().writer().writeValueAsString(ApiResponse.fail(400, msg)).getBytes(StandardCharsets.UTF_8));
    out.flush();
    out.close();
}
Copy the code

JWT and Spring Security or fragmented time, need through WebSecurityConfigurerAdapter addFilterBefore will configure method in the filter added to just go, Let’s configure the webSecurityconfig. Java file:

package com.foxescap.wxbox.config;

import com.foxescap.wxbox.filter.JwtAuthenticationFilter;
import com.foxescap.wxbox.service.AdminService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

/ * * *@author xfly
 */
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    private final AdminService adminService;

    private final JwtAuthenticationFilter jwtAuthenticationFilter;

    public WebSecurityConfig(AdminService adminService, JwtAuthenticationFilter jwtAuthenticationFilter) {
        this.adminService = adminService;
        this.jwtAuthenticationFilter = jwtAuthenticationFilter;
    }

    @Override
    public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
        authenticationManagerBuilder.userDetailsService(adminService).passwordEncoder(passwordEncoder());
    }

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

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean(a) throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .cors()
                .and()
                .authorizeRequests()
                .antMatchers("/admin/**").authenticated() .anyRequest().permitAll(); http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); }}Copy the code

Implementing the login interface

package com.foxescap.wxbox.controller;

import com.foxescap.wxbox.common.ApiCode;
import com.foxescap.wxbox.common.ApiResponse;
import com.foxescap.wxbox.common.JwtUtil;
import com.foxescap.wxbox.dto.AdminInfoDto;
import com.foxescap.wxbox.dto.param.AdminLoginParam;
import com.foxescap.wxbox.service.AdminService;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import java.util.stream.Collectors;

/ * * *@author xfly
 */
@RestController
@Validated
public class AdminController {

    private final AuthenticationManager authenticationManager;

    private final AdminService adminService;

    private final JwtUtil jwtUtil;

    public AdminController(AuthenticationManager authenticationManager, AdminService adminService, JwtUtil jwtUtil) {
        this.authenticationManager = authenticationManager;
        this.adminService = adminService;
        this.jwtUtil = jwtUtil;
    }

    @PostMapping("/auth/admin")
    public ApiResponse<Object> login(@RequestBody @Valid AdminLoginParam param, HttpServletRequest request) {

        try {
            authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(param.getUsername(), param.getPassword()));
        } catch (BadCredentialsException e) {
            return ApiResponse.fail(ApiCode.API_USERNAME_PASSWORD_UNMATCHED);
        }

        UserDetails userDetails = adminService.loadUserByUsername(param.getUsername());

        adminService.login(userDetails.getUsername(), request.getRemoteAddr());

        String token = jwtUtil.createToken(userDetails);

        var data = new AdminInfoDto();
        data.setToken(token);
        data.setUsername(userDetails.getUsername());
        data.setRoles(userDetails.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList()));
        return ApiResponse.success(data);
    }

    @GetMapping("/admin/info")
    public ApiResponse<AdminInfoDto> getInfo(a) {
        returnApiResponse.success(adminService.getInfo()); }}Copy the code

For example, the above INFO interface needs to be authenticated before entering the specific service logic. If you need to obtain the information of the current login user in the service logic, you can obtain it in the following ways:

SecurityContextHolder.getContext().getAuthentication();
Copy the code

We’ve only covered some of the basic features of Spring Security for now, but we’ll have to dig deeper.

The way ahead is so long without ending, yet high and low I’ll search with my will unbending.