清晰明了的使用Spring Security+JWT实现接口授权

      在系统中将自己的API接口给相关系统调用是很平常的事,根据需求给不同的接口划分一定的权限级别也正常不过,所以我们一般会对所调用接口做一个授权动作,相当于是一个登录操作,只能登录了系统才可以进行后续的接口调用。目前比较流行的方案有几种:

  1. 用户名和密码鉴权,使用Session保存用户鉴权结果。
  2. 使用OAuth进行鉴权(其实OAuth也是一种基于Token的鉴权,只是没有规定Token的生成方式)
  3. 自行采用Token进行鉴权

      第一种在分布式系统中需要借助第三方存储介质如redis来维护session的状态,第二种OAuth的方案和JWT的方案都是基于Token的,但是其实现较为复杂,今天我们主要了解的是:JWT基于Token的鉴权。

一、什么是JWT

      JWT是Json Web Token的缩写,基于 RFC 7519 标准定义的一种可以安全传输的小巧和自包含的JSON对象。由于数据是使用数字签名,所以是可信任的和安全的。JWT可以使用HMAC算法对secret进行加密或者使用RSA的公钥私钥对来进行签名。

二、JWT的工作流程

  1. 用户发送用户名、密码进行登录操作;
  2. 服务器验证用户发送的用户名、密码是否正确,如果用户信息合法,则在服务器端根据相关规则生成JWT token返回给用户
  3. 用户得到服务器端返回token后,存在客户端的localStorage、cookie或其它数据存储形式中
  4. 以后用户请求服务端其他接口时,需要在请求的header中加入Authorization:Bearer xxxx(token)。(注意token之前有一个7字符长度的 Bearer)
  5. 服务器端对用户传过来的token进行校验,如果合法,则获取token中相关信息,响应请求的数据
  6. 用户获取所请求的数据

此处的token必须要在每次请求时传递给服务端,它应该保存在请求头里, 另外,服务端要支持CORS(跨来源资源共享)策略,一般我们在服务端这么做就可以了Access-Control-Allow-Origin: *。
在这里插入图片描述
JWT所生成的token的结构如下,

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

由三部分组成,每部分用 . 分隔,每段都是用 Base64 编码的。可以使用Base64的解码器进行解析。
第一部分:header
JWT的头部包含两部分信息:

  • 声明类型,这里是jwt
  • 声明加密的算法 通常直接使用 HMAC SHA256
    上述token中的第一部分, eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 被解析成了:
{
    "alg": "HS256",
    "typ": "JWT"
}

第二部分: playload
存放有效信息的部分,包括有:

  • 标准中注册的声明
  • 公共的声明
  • 私有的声明

其中:
标准中注册的声明 (建议但不强制使用) :

  • iss: jwt签发者
  • sub: jwt所面向的用户
  • aud: 接收jwt的一方
  • exp: jwt的过期时间,这个过期时间必须要大于签发时间
  • nbf: 定义在什么时间之前,该jwt都是不可用的.
  • iat: jwt的签发时间
  • jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

公共的声明 :
公共的声明可添加任何的信息,一般添加用户相关信息或其他业务需要的必要信息。但不建议添加敏感信息,因为该部分在客户端可解密。

私有的声明 :
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息。
上述token中的第二部分,通过Base64的解码器解码后,数据如下:

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

第三部分:signature
签证信息,由三部分组成:

  • header (base64后的)
  • payload (base64后的)
  • secret
    这部分使用base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,构成jwt的第三部分。这个地方进行解析需要secret私钥才能计算,这个地方就是JWT的安全保障。
// javascript
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
var signature = HMACSHA256(encodedString, 'secret');

将这三部分用.连接成一个完整的字符串,构成了最终的JWT。
注意:

  1. secret是保存在服务器端的,JWT的签发生成也是在服务器端的,secret就是用来进行jwt的签发和JWT的验证,所以,它相当于服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发JWT了。
  2. 基于JWT对于API的权限划分、资源的权限划分,用户的验证等等不是JWT负责的。JWT相当于只负责登录操作,登录成功后,用户所对应的权限用户角色决定的。

三、什么是Spring Security

Spring Security是一个基于Spring的通用安全框架,能够为 Spring企业应用系统提供声明式的安全访问控制。

  1. Spring Security 提供了若干个可扩展的、可声明式使用的过滤器处理拦截的web请求。(本例使用此功能)
    在web请求处理时, Spring Security框架根据请求url和声明式配置,筛选出合适的一组过滤器集合拦截处理当前的web请求。这些请求会被转给Spring Security的安全访问控制框架处理通过之后,请求再转发应用程序处理,从而增强了应用的安全性。
  2. Spring Security 提供了可扩展的认证、鉴权机制对Web请求进行相应对处理。
    认证:识别并构建用户对象,如:根据请求中的username,获取登录用户的详细信息,判断用户状态,缓存用户对象到请求上下文等。
    决策:判断用户能否访问当前请求,如:识别请求url,根据用户、权限和资源(url)的对应关系,判断用户能否访问当前请求url。

四、使用Spring Security+JWT实现接口授权实战

1.创建springboot工程,添加相关依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
2.测试springboot项目请求正常

controller类:

@RestController
@RequestMapping("/api")
public class TestController {
    @GetMapping("/test")
    public String test() {
        return "Send success!";
    }
}

可以看出此时没有加校验时,无论是谁都可以请求成功。
在这里插入图片描述

3.添加授权机制基础类

(1)添加Spring Security+JWT的依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.20</version>
</dependency>
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.3.2</version>
</dependency>

(2)创建数据表user表

CREATE TABLE `t_user`  (
  `id` bigint(32) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '用户名',
  `password` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '密码',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = Dynamic;

(3)创建user实体

@Data
@TableName("t_user")
public class User {
    @TableId(type = IdType.AUTO)
    private long id;
    private String name;
    private String password;
}

(4)用户信息插入及查询接口

public interface IUserService {
    void save(User user);
}
/**
 * 用户操作实现类
 */
@Service
public class UserServiceImpl implements IUserService {
    @Resource
    private UserMapper userMapper;
    @Override
    public void save(User user) {
        userMapper.insert(user);
    }
}
/**
 * 用户数据库操作接口,继承mybatis-plus BaseMapper类
 */
@Mapper
public interface UserMapper extends BaseMapper<User> {
}

(5)用户注册接口

@Resource
private IUserService userService;
@Resource
private BCryptPasswordEncoder bCryptPasswordEncoder;
@PostMapping("/signup")
public void signUp(@RequestBody User user) {
	// 对用户密码进行加密存储,此处使用BCryptPasswordEncoder进行加密,
	// BCryptPasswordEncoder类在Application中定义
    user.setPassword(bCryptPasswordEncoder.encode(user.getPassword()));
    userService.save(user);
}

@SpringBootApplication
public class StageApplication {
   @Bean
   public BCryptPasswordEncoder bCryptPasswordEncoder() {
       return new BCryptPasswordEncoder();
   }

   public static void main(String[] args) {
       SpringApplication.run(StageApplication.class, args);
   }
}
4.添加JWT认证功能

用户填入用户名密码后,与数据库里存储的用户信息进行比对,如果通过,则认证成功。认证通过后,服务器生成一个token,将token返回给客户端,客户端以后的所有请求都需要在http头中指定该token。服务器接收的请求后,会对token的合法性进行验证。
(1)类JWTLoginFilter,核心功能是在验证用户名密码正确后,生成一个token,并将token返回给客户端。

package com.kelly.stage.filter;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.kelly.stage.entity.User;
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.web.authentication.UsernamePasswordAuthenticationFilter;

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

/**
 * 该类继承自UsernamePasswordAuthenticationFilter,重写其中的2个方法
 * attemptAuthentication :接收并解析用户凭证。
 * successfulAuthentication :用户成功登录后,调用此方法生成token。
 *
 * @author Kelly
 * @version v1.0
 * @date 2020/6/26 10:42
 */
public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter {
    private AuthenticationManager authenticationManager;

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

    /**
     * 接收并解析用户凭证	
     */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res) {
        try {
            User user = new ObjectMapper().readValue(req.getInputStream(), User.class);
            return authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(user.getName(), user.getPassword(), new ArrayList<>())
            );
        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException();
        }
    }

    /**
     * 用户成功登录后,生成token
     */
    @Override
    protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, 
    										FilterChain chain, Authentication auth) {
        String token = Jwts.builder()
        		.setSubject(((org.springframework.security.core.userdetails.User)auth.getPrincipal()).getUsername())
                .setExpiration(new Date(System.currentTimeMillis() + 60 * 60 * 24 * 1000))
                .signWith(SignatureAlgorithm.HS512,"MyJwtSecret")
                .compact();
        res.addHeader("Authorization","Bearer " + token);
    }
}

(2)授权验证,用户登录成功后拿到token,后续的请求都会带着token,服务端会验证token的合法性。此处创建JwtAuthenticationFilter类。

package com.kelly.stage.filter;

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;

/**
 * JwtAuthenticationFilter
 * 实现token的校验功能。
 * 继承BasicAuthenticationFilter,在doFilterInternal方法中,从http头的Authorization项读取token数据,
 * 然后用Jwts包提供的方法校验token的合法性。如果校验通过,就认为这是一个取得授权的合法请求
 *
 * @author Kelly
 * @version v1.0
 * @date 2020/6/26 10:54
 */
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
    public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException {
        String header =  req.getHeader("Authorization");
        if (header == null || !header.startsWith("Bearer ")) {
            chain.doFilter(req, res);
            return;
        }

        UsernamePasswordAuthenticationToken authenticationToken = getAuthentication(req);
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        chain.doFilter(req, res);
    }

    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
        String token = request.getHeader("Authorization");
        if (token != null) {
            String user = Jwts.parser()
                    .setSigningKey("MyJwtSecret")
                    .parseClaimsJws(token.replace("Bearer ",""))
                    .getBody()
                    .getSubject();
            if (user != null) {
                return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>());
            }
            return null;
        }
        return null;
    }
}

(3)SpringSecurity配置,将JWTLoginFilter,JWTAuthenticationFilter组合在一起

package com.kelly.stage.config;

import com.kelly.stage.filter.JwtAuthenticationFilter;
import com.kelly.stage.filter.JwtLoginFilter;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpMethod;
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;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

/**
 * WebSecurityConfig
 * 通过SpringSecurity配置,将JWTLoginFilter,JWTAuthenticationFilter组合在一起
 *
 * @author Kelly
 * @version v1.0
 * @date 2020/6/26 11:04
 */
@Configuration
@Order
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    private UserDetailsService userDetailsService;

    private BCryptPasswordEncoder bCryptPasswordEncoder;

    public WebSecurityConfig(UserDetailsService userDetailsService, BCryptPasswordEncoder bCryptPasswordEncoder) {
        this.userDetailsService = userDetailsService;
        this.bCryptPasswordEncoder = bCryptPasswordEncoder;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable().authorizeRequests()
                .antMatchers(HttpMethod.POST, "/api/signup").permitAll()
                .antMatchers(HttpMethod.POST,"/login").permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .addFilter(new JwtLoginFilter(authenticationManager()))
                .addFilter(new JwtAuthenticationFilter(authenticationManager()));
    }

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

(4)账号密码验证,创建UserDetailsServiceImpl,实现security中的UserDetailsService接口

package com.kelly.stage.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.kelly.stage.entity.User;
import com.kelly.stage.mapper.UserMapper;
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 javax.annotation.Resource;
import java.util.ArrayList;

/**
 * UserDetailsServiceImpl
 *
 * @author Kelly
 * @version v1.0
 * @date 2020/6/26 12:26
 */
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Resource
    private UserMapper userMapper;
    @Override
    public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {
        QueryWrapper<User> query = new QueryWrapper<>();
        query.eq("name", name);
        User user = userMapper.selectOne(query);
        if(user == null){
            throw new UsernameNotFoundException(name);
        }
        return new org.springframework.security.core.userdetails.User(user.getName(), user.getPassword(), new ArrayList<>());
    }
}

(5)功能验证
① 再次请求localhost:8080/stage/api/test,此时没有登录授权,会出现403 Forbidden提示。
在这里插入图片描述
② 注册用户信息,请求localhost:8080/stage/api/signup地址,向数据库中插入用户信息。
在这里插入图片描述
③ 用户登录,直接请求localhost:8080/stage/login地址,服务端返回token,在http header中,Authorization: Bearer 后面的部分就是token,用于后续接口请求。(注意:login接口是springsecurity自带的,不需要实现)。
在这里插入图片描述
④ 再次请求localhost:8080/stage/api/test,此时带着token授权,则可成功请求。
在这里插入图片描述
至此,使用Spring Security+JWT实现接口授权功能就实现了,主要是JwtLoginFilterJwtAuthenticationFilterWebSecurityConfig的实现,注意不要忘记了账号密码验证的UserDetailsServiceImpl实现类。

五、总结

  1. 因为json的通用性,所以JWT是可以进行跨语言支持;
  2. 在payload部分,JWT存储一些其他业务逻辑所必要的非敏感信息;
  3. JWT的构成非常简单,字节占用很小,便于传输;
  4. JWT不需要在服务端保存会话信息, 所以它易于应用的扩展;
  5. 不应该在JWT的payload部分存放敏感信息,因为payload在客户端可解密;
  6. secret私钥不可泄漏;
六、参考:
  1. Spring Boot+Spring Security+JWT 实现 RESTful Api 权限控制
  2. 重拾后端之Spring Boot(四):使用JWT和Spring Security保护REST API
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值