【SpringSecurity】SpringSecurity2.7.x 的使用(04)


前后完全分离下认证和授权
在这里插入图片描述

有两种方式可以解决前后端分离

  1. 生成随机字符串存入redis中,key为随机字符串,value为用户信息,每次请求携带这个唯一的字符串,通过查询redis来验证过是否为合法登录
  2. 使用JWT生成token,JWT也是生成一串字符串,通过一定的加密方式以及一定的标记(签名sign)生成的,解析时也要通过相同的方式来解析

三、 前后完全分离下认证和授权

3.1 什么是JWT

Json web token (JWT),是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。

3.2 JWT的原理

JWT的原理是,服务器认证以后,生成一个JSON对象,发回给用户,就像下面这样。

{
	"姓名":"张三""角色":"管理员",
	"到期时间":"2022年8月1日0点0分"
}

以后,用户与服务端通信的时候,都要发回这个JSON对象。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名(详见后文)。
服务器就不保存任何 session 数据了,也就是说,服务器变成无状态了,从而比较容易实现扩展。

3.3 JWT的数据结构

实际的 JWT大概就像下面这样。
在这里插入图片描述
它是一个很长的字符串,中间用点(.)分隔成三个部分。注意,JWT内部是没有换行的,这里只是为了便于展示,将它写成了几行。
JWT的三个部分依次如下。

  1. Header (头部)
  2. Payload(负载 载荷)
  3. Signature(签名)

写成一行,就是下面的样子。
Header.Payload.Signature

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpbkluZm8iOnsiYXV0aG9yaXRpZXMiOlsiL3VzZXIvZGVsZXRlIiwiL3VzZXIvaW5zZXJ0IiwiL3VzZXIvcXVlcnkiLCIvdXNlci91cGRhdGUiXSwidXNlcm5hbWUiOiJ6aGFuZ3NhbiJ9LCJleHAiOjE2Njc0ODY1MjAsImlhdCI6MTY2NzQ3OTM2Nn0.k2gyEU6xRn-sPahRAZXKskenX4814Nbp0msdDbZyg-o

3.3.1 Header

Header 部分是一个JSON对象,描述JWT的元数据,通常是下面的样子。

{
	"alg": "HS256",
	"typ": "JWT"
}

上面代码中,alg属性表示签名的算法(algorithm),默认是 HMAC SHA256 (写成 HS256) ;typ属性表示这个令牌(token)的类型(type), JWT令牌统一写为JWT。
最后,将上面的JSON对象使用Base64URL算法转成字符串。

3.3.2 Payload

Payload 部分也是一个JSON对象,用来存放实际需要传递的数据。JWT规定了7个官方字段,供选用。
iss (issuer):签发人
exp (expiration time):过期时间

sub (subject):主题
aud (audience):受众

nbf (Not Before):生效时间

iat (lssued At):签发时间

jti (JWT ID):编号
除了官方字段,你还可以在这个部分定义私有字段,下面就是一个例子。

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

注意,JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。这个JSON 对象也要使用Base64URL 算法转成字符串。

3.3.3 Signature

Signature部分是对前两部分的签名,防止数据篡改。
首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用Header里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。

HMACSHA256(base64UrlEncode(header)
				 + ".”"+base64UrlEncode(payload),secret)

算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。

3.4 编写JWT工具类

1. 引入jar包

<!--JWT的依赖-->
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.18.3</version>
</dependency>

2. 编写工具类

package com.aaa.springsecuritydemo02.utils;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;

import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * @author : 尚腾飞
 * @version : 1.0
 * @createTime : 2022/11/2 15:38
 * @description : jwt工具类
 */
public class JwtUtils {
    /**
     * 签名:很关键!只有服务器知道,不要告诉其他人,
     * 知道签名和加密算法后是可以解析出内容的
     */
    private static  String sign = "ykq-aaa";

    /**
     * 获取token
     * @param map
     * @return
     */
    public static String getJwtToken(Map map){
        Map<String,Object> head = new HashMap<>();
        head.put("alg","HS256");
        head.put("typ","JWT");
        //定义颁发时间
        Date iat = new Date();
        //过期时间
        Calendar expire = Calendar.getInstance();
        expire.set(Calendar.SECOND,7200);
        Date expireTime = expire.getTime();

        return JWT.create()
                //设置头信息
                .withHeader(head)
                //设置颁发时间
                .withIssuedAt(iat)
                //设置过期时间
                .withExpiresAt(expireTime)
                //自定义的内容
                .withClaim("loginInfo", map)
                //签名
                .sign(Algorithm.HMAC256(sign));
    }

    /**
     * 验证token是否有效
     * @param token
     * @return
     */
    public static boolean verifyToken(String token){
        JWTVerifier build = JWT.require(Algorithm.HMAC256(sign)).build();
        try {
            //调用校验功能,校验失败会报错
            build.verify(token);
            return true;
        } catch (JWTVerificationException e) {
            //校验失败,return false
            System.out.println("token无效");
            return false;
        }
    }

    /**
     * 从token中获取相关的载荷内容
     * @param token
     * @return
     */
    public static Map<String, Object> getTokenChaim(String token){
        //获取一个JWT校验对象
        JWTVerifier build = JWT.require(Algorithm.HMAC256(sign)).build();
        //调用校验功能
        DecodedJWT verify = build.verify(token);
        //取出我们上面存的载荷
        Claim claim = verify.getClaim("loginInfo");
        //返回map
        return claim.asMap();
    }
}

3.5 修改登录成功后返回json的方法

//认证
http
        .formLogin()
        //登录的处理路径,无需自己创建该路径的业务处理功能。
        .loginProcessingUrl("/login")
        //登录成功返回json
        .successHandler((request, response, authentication) -> {
            //设置返回类型
            response.setContentType("application/json;charset=utf-8");
            PrintWriter writer = response.getWriter();

            //从authentication中拿到登录的用户信息,强转为类型User
            User principal = (User) authentication.getPrincipal();
            //从用户信息中拿到权限信息
            Collection<GrantedAuthority> authorities = principal.getAuthorities();
            //将权限信息转换为list集合
            List<String> list = authorities.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());
            //定义一个map,用来存放用户信息
            Map<String, Object> map = new HashMap<>();
            //将用户名放入map
            map.put("username", principal.getUsername());
            //将权限信息放入map
            map.put("authorities", list);
            //通过工具类获取token
            String jwtToken = JwtUtils.getJwtToken(map);
            //将token返回
            Result result = new Result(200, "登录成功", jwtToken);
            String jsonStr = JSONUtil.toJsonStr(result);
            writer.print(jsonStr);

            writer.flush();
            writer.close();
        })

登录成功后将token返回给前端,以后前端每次请求都要携带这个token,我们在后端写一个过滤器,每次有请求的时候都要验证是否为合法用户!

3.6 创建一个过滤器类并继承OncePerRequestFilter父类

package com.aaa.springsecuritydemo02.filter;

import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONUtil;
import com.aaa.springsecuritydemo02.entity.Result;
import com.aaa.springsecuritydemo02.utils.JwtUtils;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
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;
import java.io.PrintWriter;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * @author : 尚腾飞
 * @version : 1.0
 * @createTime : 2022/11/3 15:26
 * @description :
 */
@Component
public class JwtVerifyFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        response.setContentType("application/json;charset=utf-8");
        //获取token
        String token = request.getHeader("token");
        //获取请求路径
        String path = request.getRequestURI();
        //获取请求方式
        String method = request.getMethod();
        //是否为登录请求
        if ("/login".equals(path) && "POST".equals(method)) {
            filterChain.doFilter(request, response);
            return;
        }
        //判断是否传入了token
        if (ObjectUtil.isNotEmpty(token)) {
            //使用工具类,验证token是否有效
            boolean verifyToken = JwtUtils.verifyToken(token);
            if (verifyToken) {
                //获取载荷信息
                Map<String, Object> tokenChaim = JwtUtils.getTokenChaim(token);
                //从载荷中获取登录的用户名
                String username = tokenChaim.get("username").toString();
                //从载荷中获取登录者的权限
                List<String> list = (List<String>) tokenChaim.get("authorities");
                //将权限转换为list
                List<SimpleGrantedAuthority> authorities = list.stream().map(item -> new SimpleGrantedAuthority(item)).collect(Collectors.toList());

                //把解析的结果封装到SecurityContext中,可以交于security判断相应的权限
                UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, null, authorities);
                SecurityContext context = SecurityContextHolder.getContext();
                context.setAuthentication(authenticationToken);
                //放行
                filterChain.doFilter(request, response);
                return;
            }

        }
        //token为null或者token无效时。响应一个json数据.
        PrintWriter writer = response.getWriter();
        Result result = new Result(500, "token无效", null);
        String jsonStr = JSONUtil.toJsonStr(result);
        
        writer.print(jsonStr);

        writer.flush();
        writer.close();
    }
}

过滤器的整体思想就是校验token的合法性

  1. 判断请求路径是否为登录请求,如果是登录请求肯定是没有token的,直接放行。
  2. 校验token,如果token不为空并且是有效的,就拿到载荷中的信息,封装到SecurityContext中,封装时 构造方法有三个参数,一个是用户名一个是权限,密码传null就行。

修改security的配置类
在这里插入图片描述

  • 将自己的过滤器放到UsernamePasswordAuthenticationFilter过滤器前面。
  • 我们自己定义的过滤器里面,判断是否为登录请求,如果是登录请求就去下面的过滤器去登录的。登录过滤器在前面,就没有意义了。

3.7 使用脚手架创建vue项目框架

创建vue项目时有一点就是把路由选上就行了,如果不想要严格的语法定义,把语法那一项关闭。具体步骤略…(之前笔记里有写)

3.7.1 安装element-ui 和 axios

在脚手架框架中要想使用element首先得安装

cnpm i element-ui -S

并在main.js中添加

import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';

//让Vue引入使用ElementUI
Vue.use(ElementUI);

axios简介和安装

axios的优点:
易用、简洁且高效的http库。 支持原始JS中的Promise,做异步请求数据工具。

向后端发送Ajax请求,安装axios

cnpm  i  --save axios
import axios from 'axios'

//设置路径的默认前缀
axios.defaults.baseURL="http://localhost:9090"
//把axios挂载到vue对象
Vue.prototype.$http=axios;


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

打乒乓球只会抽

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值