SpringBoot+JWT+SpringSecurity对api进行授权保护

概述

在生成环境中,我们通常需要对接口加入一些权限认证。我们可以使用session来记录当前请求用户的状态信息加以验证,但是当服务架构从单一演化到集群模式的时候,又会出现session共享问题。

今天我们使用jwt对来解决接口权限认证的问题,在我看来jwt有如下好处。
1. 无状态,不需要占用服务器的资源。
2. 紧凑安全,特别适用于SSO场景,不管单机或者集群,都适用。
3. 高效,无需多次验证,在一次会话中,只需要验证一次。

 

jwt简介

什么是jwt

JWT(Json Web Token),是一种工具,格式为XXXX.XXXX.XXXX的字符串,JWT以一种安全的方式在用户和服务器之间传递存放在JWT中的不敏感信息。

jwt的构成

jwt是由三部分构成的`Header.Payload.Signature`

header:存放我们的jwt使用生成singnature的算法类型

Payload:Claim里面存放了jwt自身的一些属性,这些属性是用来标注用户的身份属性,比如:JWT的签发着,JWT的创建时间,失效时间,或者用户自定义的属性等等(以下是一个完整的claime)。claime通过Base64转码生成后的字符串称为Payload。

{ 
    "iss":"Issuer —— 用于说明该JWT是由谁签发的", 
    "sub":"Subject —— 用于说明该JWT面向的对象", 
    "aud":"Audience —— 用于说明该JWT发送给的用户", 
    "exp":"Expiration Time —— 数字类型,说明该JWT过期的时间", 
    "nbf":"Not Before —— 数字类型,说明在该时间之前JWT不能被接受与处理", 
    "iat":"Issued At —— 数字类型,说明该JWT何时被签发", 
    "jti":"JWT ID —— 说明标明JWT的唯一ID", 
    "user-definde":"用户自定义属性举例"
}

Signature:将上面的Header和Payload以`Header.Payload`的格式组合在一起形成一个字符串,然后使用header里面的加密算法和一个密匙(这个密匙存放在服务器上,用于进行验证)对这个字符串进行加密,形成一个新的字符串,这个字符串就是Signature。

我们可以登录 https://jwt.io 来decoded我们的jwt

SpringBoot+JWT+SpringSecurity对api进行授权保护

 

jwt认证过程

SpringBoot+JWT+SpringSecurity对api进行授权保护SpringBoot+JWT+SpringSecurity对api进行授权保护

代码实现

引入jar包

<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>

jwt生成过程

创建一个`JWTLoginFilter`过滤器,创建之后该方法就会默认的过滤客户端的"/login"请求,它主要用于在验证用户名和密码正确之后,生成一个token返回给客户端。该类继承自`UsernamePasswordAuthenticationFilter`主要实现了它的两个方法:

`attemptAuthentication`:解析用户传来的用户信息。

`successfulAuthentication`:用户登陆请求成功校验之后,这个类会被调用,用于生成token

package com.wintig.jwt.filter;

import com.alibaba.fastjson.JSON;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.wintig.jwt.entity.MyUser;
import com.wintig.jwt.entity.Response;
import com.wintig.jwt.utils.DateUtils;
import com.wintig.jwt.utils.ResponseUtil;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;

public class JWTLoginFilter extends UsernamePasswordAuthenticationFilter {

    private AuthenticationManager authenticationManager;

    public JWTLoginFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest req,
                                                HttpServletResponse res) throws AuthenticationException {
        try {

            //获取前台过来的user信息
            MyUser myUser = new ObjectMapper()
                    .readValue(req.getInputStream(), MyUser.class);


            return authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(
                            myUser.getUsername(),
                            myUser.getPassword(),
                            new ArrayList<>())
            );

        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }


    /**
     * 请求校验成功后的处理
     */
    @Override
    protected void successfulAuthentication(HttpServletRequest req,
                                            HttpServletResponse res,
                                            FilterChain chain,
                                            Authentication auth) {

        HashMap<String, String> result = new HashMap<>();

        String token = Jwts.builder()
                .setSubject(((User) auth.getPrincipal()).getUsername())
                .setIssuedAt(new Date())
                .setExpiration(DateUtils.LocalDateTimeToDate(LocalDateTime.now().plusDays(7))) //设置有效期7天
                .signWith(SignatureAlgorithm.HS512, "jwtSecurity")
                .compact();

        result.put("Authorization", "Bearer " + token);

        //将生成token返回给前台
        ResponseUtil.write(res, JSON.toJSON(Response.success(result)));
    }

}

创建一个实现了`UserDetailsService`方法的类`UserDetailsServiceImpl`,这个类用于处理我们根据前台传来的`username`,来获取我们数据库中的真实用户的信息并返回给`JWTManager`,让jwt来验证我们的用户信息是否正确。
 

package com.wintig.jwt.service;


import com.wintig.jwt.dao.MyUserRepository;
import com.wintig.jwt.entity.MyUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import static java.util.Collections.emptyList;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private MyUserRepository myUserRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 查询我们的真实用户,返回给前台
        MyUser user = myUserRepository.findByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException(username);
        }
        return new User(user.getUsername(), user.getPassword(), emptyList());
    }
}

jwt授权验证过程

当用户登录成功后,会拿到token,在之后所有的请求,带上这个token,服务端就会验证token的合法性。

创建一个`JWTAuthenticationFilter`类,这个类主要用于进行token的校验,`doFilterInternal`方法中,获取http请求头中的`Authorization`读取token数据

package com.wintig.jwt.filter;


import com.alibaba.fastjson.JSON;
import com.wintig.jwt.entity.Response;
import com.wintig.jwt.enums.ResultStatusEnum;
import com.wintig.jwt.utils.ResponseUtil;
import io.jsonwebtoken.Jwts;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;

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


public class JWTAuthenticationFilter extends BasicAuthenticationFilter {

    public JWTAuthenticationFilter(AuthenticationManager authManager) {
        super(authManager);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest req,
                                    HttpServletResponse res,
                                    FilterChain chain) throws IOException, ServletException {
        String header = req.getHeader("Authorization");

        // 如果没有带token
        if (header == null || !header.startsWith("Bearer ")) {
            chain.doFilter(req, res);
            return;
        }

        UsernamePasswordAuthenticationToken authentication;

        try {
            authentication = getAuthentication(req);
        } catch (Exception e) {
            //如果校验token的时候出现了比如格式问题,或者过期了等问题,返回校验错误结果
            ResponseUtil.write(res, JSON.toJSON(Response.failed(ResultStatusEnum.ACCESS_DENIED, "Token Check Error")));
            return;
        }

        SecurityContextHolder.getContext().setAuthentication(authentication);
        chain.doFilter(req, res);
    }

    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {

        String token = request.getHeader("Authorization");

        if (token != null) {
            // 解析token.
            String user = Jwts.parser()
                        .setSigningKey("jwtSecurity")
                        .parseClaimsJws(token.replace("Bearer ", ""))
                        .getBody()
                        .getSubject();

            // token验证成功
            if (user != null) {
                return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>());
            }

            return null;
        }
        return null;
    }

}

SpringSecurity配置

下面就是SpringSecurity的配置,没什么好说的了,在`antMatchers`方法中加入我们需要进行验证的接口,permitAll就说明当前接口可以具有所有的权限,不需要验证。如果想做更细的验证的话,看看方法名就差不多了。不过现在的互联网项目,大多数做一个登录验证就满足了大部分的需求

package com.wintig.jwt.config;

import com.wintig.jwt.filter.JWTAuthenticationFilter;
import com.wintig.jwt.filter.JWTLoginFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
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.core.userdetails.UserDetailsService;

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable().authorizeRequests()
                .antMatchers(
                        "/user/register"
                ).permitAll()
                .anyRequest().authenticated()
                .and()
                .addFilter(new JWTLoginFilter(authenticationManager()))
                .addFilter(new JWTAuthenticationFilter(authenticationManager()));
    }

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService);
    }

}

演示

首先我们随便访问一个没有开放权限的接口`/user/hello`,然后报了403错误

SpringBoot+JWT+SpringSecurity对api进行授权保护

下面我们注册一个账户,从上面的配置可以看出用户注册接口`/user/register`,不需要进行权限验证,可以直接访问。我们注册一个账户,接口顺利的就注册成功了。

SpringBoot+JWT+SpringSecurity对api进行授权保护

接下来我们调用`/login`方法,使用我们刚注册的用户进行登陆,可以看到接口给我们返回了token

SpringBoot+JWT+SpringSecurity对api进行授权保护

SpringBoot+JWT+SpringSecurity对api进行授权保护

下面我们根据拿到的token去访问,我们之前没有权限的接口`/user/hello`,这时候会发现接口成功请求了。

SpringBoot+JWT+SpringSecurity对api进行授权保护

总结

以上一个简单的SpringBoot+JWT为API增加授权保护就顺利的完成的,还是很简单的。基本上可以满足日常绝大部分的需求,如果需要更复杂的权限校验,就需要查阅其他教程。

源码地址:https://github.com/wintig/spring-boot-examples/tree/master/spring-boot-jwt