当前位置:网站首页>Kill session? This cross domain authentication solution is really elegant

Kill session? This cross domain authentication solution is really elegant

2022-06-12 00:40:00 Silent King II

User login authentication is Web A very common business in applications , The general process is like this :

  • The client sends the user name and password to the server
  • After the server-side verification passes , In the current session (session) Save relevant data in , For example, login time 、 Sign in IP etc. .
  • The server returns a... To the client session_id, The client saves it in Cookie in .
  • When the client sends a request to the server again , take session_id Send it back to the server .
  • On the server side session_id after , Identify the user .

In the case of a single machine , There is no problem with this model , But for front and rear end separation Web applications , It's very painful . So there is another solution , The server side will no longer save session data , Instead, save it on the client , Each time the client initiates a request, it sends the data to the server for verification .JWT(JSON Web Token) It is a typical representative of this scheme .

One 、 About JWT

JWT, It's the most popular Cross domain Certification solutions : The client initiates a user login request , After the server receives and authenticates successfully , Generate a JSON object ( As shown below ), And then it's returned to the client .

{
  "sub": "wanger",
  "created": 1645700436900,
  "exp": 1646305236
}

When the client communicates with the server again , Put this JSON Take the object with you , As a certificate of mutual trust between the front and back ends . After the server receives the request , adopt JSON Object to authenticate the user , This eliminates the need to save any session Data. .

If I use a user name now wanger And password 123456 Access programming meow (Codingmore) Of login Interface , So practical JWT It's a string that looks like it's encrypted .

In order to let everyone see more clearly , I copied it to jwt Its official website .

left Encoded Part is JWT Ciphertext , Intermediate use 「.」 Divided into three parts ( On the right side Decoded part ):

  • Header( Head ), describe JWT Metadata , among alg The algorithm of attribute representation signature ( At present, it is HS512);
  • Payload( load ), It is used to store the data that needs to be transferred , among sub Property represents the subject ( The actual value is user name ),created Attribute representation JWT Time of birth ,exp Property indicates the expiration time
  • Signature( Signature ), Signature of the first two parts , Prevent data tampering ; You need to specify a key on the server side ( Only the server side knows ), Can't leak to client , And then use Header The signature algorithm specified in , Follow the formula below to generate a signature :
HMACSHA512(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  your-256-bit-secret
)

After you figure out the signature , And then Header、Payload、Signature Concatenate into a string , Intermediate use 「.」 Division , You can return it to the client .

The client gets JWT after , Can be placed in localStorage, It can also be placed in Cookie Inside .

const TokenKey = '1D596CD8-8A20-4CEC-98DD-CDC12282D65C' // createUuid()

export function getToken () {
  return Cookies.get(TokenKey)
}

export function setToken (token) {
  return Cookies.set(TokenKey, token)
}

When the client communicates with the server in the future , Just take this JWT, Generally placed on HTTP The header of the request Authorization In the field .

Authorization: Bearer <token>

After the server receives the request , Right again JWT To verify , If the verification is passed, the corresponding resources will be returned .

Two 、 actual combat JWT

First step , stay pom.xml Add... To the file JWT Dependence .

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>

The second step , stay application.yml Add JWT Configuration item for .

jwt:
  tokenHeader: Authorization #JWT Stored request header 
  secret: codingmore-admin-secret #JWT The key used for encryption and decryption 
  expiration: 604800 #JWT Beyond the deadline of (60*60*24*7)
  tokenHead: 'Bearer '  #JWT Get the beginning of the load 

The third step , newly build JwtTokenUtil.java Tool class , There are three main ways :

  • generateToken(UserDetails userDetails): Generated according to the logged in user token
  • getUserNameFromToken(String token): from token Get login user
  • validateToken(String token, UserDetails userDetails): Judge token Whether it is still valid
public class JwtTokenUtil {
    

    @Value("${jwt.secret}")
    private String secret;
    @Value("${jwt.expiration}")
    private Long expiration;
    @Value("${jwt.tokenHead}")
    private String tokenHead;

    /** *  According to the user information token */
    public String generateToken(UserDetails userDetails) {
    
        Map<String, Object> claims = new HashMap<>();
        claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername());
        claims.put(CLAIM_KEY_CREATED, new Date());
        return generateToken(claims);
    }

    /** *  According to the user name 、 Create time generation JWT Of token */
    private String generateToken(Map<String, Object> claims) {
    
        return Jwts.builder()
                .setClaims(claims)
                .setExpiration(generateExpirationDate())
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    /** *  from token Get login user name  */
    public String getUserNameFromToken(String token) {
    
        String username = null;
        Claims claims = getClaimsFromToken(token);
        if (claims != null) {
    
            username = claims.getSubject();
        }

        return username;
    }

    /** *  from token In order to get JWT Load in  */
    private Claims getClaimsFromToken(String token) {
    
        Claims claims = null;
        try {
    
            claims = Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
    
            LOGGER.info("JWT Format validation failed :{}", token);
        }
        return claims;
    }

    /** *  verification token Is it still valid  * * @param token  From the client token * @param userDetails  User information from the database  */
    public boolean validateToken(String token, UserDetails userDetails) {
    
        String username = getUserNameFromToken(token);
        return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
    }

    /** *  Judge token Is it invalid  */
    private boolean isTokenExpired(String token) {
    
        Date expiredDate = getExpiredDateFromToken(token);
        return expiredDate.before(new Date());
    }

    /** *  from token Get the expiration time in the  */
    private Date getExpiredDateFromToken(String token) {
    
        Claims claims = getClaimsFromToken(token);
        return claims.getExpiration();
    }
}

Step four , stay UsersController.java Newly added login Login interface , Receive user name and password , And will JWT Return to the client .

@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 = " Log in and return to token")
    @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 user name or password ");
        }

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

}

Step five , stay UsersServiceImpl.java Newly added login Method , Query the user from the database according to the user name , Generate after password verification JWT.

@Service
public class UsersServiceImpl extends ServiceImpl<UsersMapper, Users> implements IUsersService {
    

    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    public String login(String username, String password) {
    
        String token = null;
        // The password needs to be passed after being encrypted by the client 
        try {
    
            //  Query the user + User resources 
            UserDetails userDetails = loadUserByUsername(username);

            //  Verify password 
            if (!passwordEncoder.matches(password, userDetails.getPassword())) {
    
                Asserts.fail(" Incorrect password ");
            }

            //  return  JWT
            token = jwtTokenUtil.generateToken(userDetails);
        } catch (AuthenticationException e) {
    
            LOGGER.warn(" Login exception :{}", e.getMessage());
        }
        return token;
    }
}

Step six , newly added JwtAuthenticationTokenFilter.java, Every time the client makes a request, it will respond to JWT To verify .

public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    
    private static final Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class);
    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private JwtTokenUtil jwtTokenUtil;
    @Value("${jwt.tokenHeader}")
    private String tokenHeader;
    @Value("${jwt.tokenHead}")
    private String tokenHead;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) throws ServletException, IOException {
    
        //  Get... From the client request  JWT
        String authHeader = request.getHeader(this.tokenHeader);
        //  The  JWT  It's the format we stipulated , With  tokenHead  start 
        if (authHeader != null && authHeader.startsWith(this.tokenHead)) {
    
            // The part after "Bearer "
            String authToken = authHeader.substring(this.tokenHead.length());
            //  from  JWT  Get user name 
            String username = jwtTokenUtil.getUserNameFromToken(authToken);
            LOGGER.info("checking username:{}", username);

            // SecurityContextHolder  yes  SpringSecurity  A tool class of 
            //  Save the security context of the current user in the application 
            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
    
                //  Obtain login user information according to user name 
                UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
                //  verification  token  Is it overdue 
                if (jwtTokenUtil.validateToken(authToken, userDetails)) {
    
                    //  Save the logged in user in a security context 
                    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails,
                            null, userDetails.getAuthorities());
                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(authentication);

                    LOGGER.info("authenticated user:{}", username);
                }
            }
        }
        chain.doFilter(request, response);
    }
}

JwtAuthenticationTokenFilter Inherited OncePerRequestFilter, This filter ensures that a request passes only once filter, It doesn't need to be repeated . in other words , Every time the client initiates a request , The filter will execute once .

This filter is very critical , Basically, I added comments to every line of code , Yes, of course , To make sure everyone can figure out what this class does , Let me draw another flow chart , So it's clear .

SpringSecurity Is a security management framework , You can talk to Spring Boot Application seamless connection ,SecurityContextHolder Is one of the key tool classes , Hold security context information , Who is the user who saves the current operation , Whether the user has been authenticated , Key information such as permissions owned by users .

SecurityContextHolder Default used ThreadLocal Policies to store authentication information ,ThreadLocal It's characterized by the data in it , Which thread has stored , Which thread can access . This means that after different requests enter the server , There will be different Thread To deal with , For example, threads A The request 1 User information is stored in ThreadLocal, Threads B Processing request 2 You can't get the user's information when you are .

So JwtAuthenticationTokenFilter The filter will do it every time a request comes JWT Validation of the , Ensure that the request from the client is secure . then SpringSecurity The next request interface will be released . This is also JWT and Session Fundamental difference :

  • JWT You need to verify every request , And as long as JWT No expired , Even if the server is restarted , The certification is still valid .
  • Session Without expiration, it is not necessary to re verify the user information , When the server is restarted , The user needs to log in again to get a new Session.

in other words , stay JWT Under the scheme of , The key saved on the server side (secret) We must not divulge , Otherwise, the client can forge the user's authentication information according to the signature algorithm .

3、 ... and 、Swagger Add JWT verification

For back-end developers , How to be in Swagger( Integrated Knife4j Beautify ) Add JWT What about verification ?

First step , visit login Interface , Enter your user name and password to log in , Get the information returned by the server JWT.

The second step , Collect the data returned by the server tokenHead and token, Fill it in Authorize( Be careful tokenHead and token There is a space between ) Complete login authentication .

The third step , When another interface is requested ,Swagger Will automatically Authorization Send it to the server as request header information .

Step four , After the server receives the request , Will pass JwtAuthenticationTokenFilter Filter right JWT check .

Only this and nothing more , The whole process has been opened up , perfect !

Four 、 summary

To sum up , use JWT To solve the cross domain authentication in the front and rear end separation project is very smooth , This is mainly due to JSON The generality of , Can cross language ,JavaScript and Java All support ; in addition ,JWT The composition of is very simple , Very easy to transmit ; also JWT There is no need to save session information on the server side (Session), Very easy to expand .

Yes, of course , In order to ensure JWT The security of , Not in JWT Save sensitive information in , Because once the private key is compromised ,JWT It is easy to decrypt on the client ; If possible , Please use HTTPS agreement .

Reference link :

Ruan Yifeng :https://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html

Spring, summer, autumn and winter :https://segmentfault.com/a/1190000012557493

A little rain in Jiangnan :https://cloud.tencent.com/developer/article/1612175

Dearmadman:https://www.jianshu.com/p/576dbf44b2ae

mcarozheng:http://www.macrozheng.com/

Source path :

https://github.com/itwanger/coding-more


This article has been included in GitHub On the star 1.6k+ star Open source column 《Java The road to advancement of programmers 》, It is said that every excellent Java Programmers like her , Humor and wit humor 、 Easy to understand . The content includes Java Basics 、Java Concurrent programming 、Java virtual machine 、Java Enterprise development 、Java Interview and other core knowledge points . learn Java, Just recognize it Java The road to advancement of programmers .

https://github.com/itwanger/toBeBetterJavaer

star Having this warehouse means that you have become an excellent Java The potential of Engineers . You can also stamp the link below to jump to 《Java The road to advancement of programmers 》 The official website of , Start a pleasant learning journey .

https://tobebetterjavaer.com/

Nothing makes me stay —— Except for the purpose , Even if there are roses by the shore 、 There's shade 、 There is a peaceful harbor , I'm not a boat .

原网站

版权声明
本文为[Silent King II]所创,转载请带上原文链接,感谢
https://yzsam.com/2022/03/202203011445175572.html