使用JWT的SpringSecurity实现前后端分离

1. SpringSecurity完成前后端完全分离

分析:

前后端分离:响应的数据必须为JSON数据,之前响应的是网页

需要修改的代码有:

  1. 登录成功需要返回json数据
  2. 登录失败需要返回json数据
  3. 权限不足时返回json数据
  4. 未登录访问资源返回json数据

1.1 登录成功需要返回json数据

第一种方案:基于redis 实现session共享

该方案的缺点:

  1. redis压力太大
  2. 项目依赖于第三方组件

第二种方案:基于jwt【采用】

jwt帮你生成唯一标志,而且校验唯一标志。信息存放在jwt中,同时可以从jwt中获取

1.2 JWT的概述

1.2.1 什么是JWT

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

官网: https://jwt.io/introduction/
JWT就是token的一种具体实现方式,本质就是一个字符串,它将用户信息保存到一个json字符串中,然后进行编码后得到一个JWT token ,并且这个JWT token 带有签名信息,接收后可以校验是否被篡改。所以可以用于在各方之间安全地将信息作为JSON对象传输。

1.2.2 前后端完全分离认证问题

互联网服务离不开用户认证。一般流程是下面这样。

1、用户向服务器发送用户名和密码。

2、服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录

时间等等。

3、服务器向用户返回一个session_id,写入用户的Cookie。

4、用户随后的每一次请求,都会通过Cookie,将session_id传回服务器。

5、服务器收到 session_id,找到前期保存的数据,由此得知用户的身份。
这种模式的问题在于,扩展性(scaling)不好。单机当然没有问题,如果是服务器集群,或者是前后端分离的服务导向架构,就要求session 数据共享,每台服务器都能够读取session,

举例来说,A网站和B网站是同一家公司的关联服务。现在要求,用户只要在其中一个网站登录,再访问另一个网站就会自动登录,请问怎么实现?
一种解决方案是 session 数据持久化,写入数据库或别的持久层。各种服务收到请求后,都向持久层请求数据。这种方案的优点是架构清晰,缺点是工程量比较大[]。另外,持久层万一挂了,就会单点失败。

另一种方案是服务器索性不保存 session 数据了,所有数据都保存在客户端,每次请求都发回服务器。JWT就是这种方案的一个代表。

JWT:影响了网络宽带

1.2.3 JWT的原理

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

{

"姓名":"张三",

"角色":"管理员",

"到期时间":"2022年8月1日0点0分"

}

以后,用户与服务端通信的时候,都要发回这个JSON对象。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名(详见后文)。

服务器就不保存任何 session 数据了,也就是说,服务器变成无状态了,从而比较容易实现扩展。

1.2.4 JWT的数据结构

实际的 JWT大概就像下面这样。

它是一个很长的字符串,中间用点(.)分隔成三个部分。注意,JWT内部是没有换行的,这里只是为了便于展示,将它写成了几行
JWT的三个部分依次如下:

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

写成一行,就是下面的样子。

Header.Payload.Signature

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

JWT头是一个描述JWT元数据的JSON对象,alg属性表示签名使用的算法,默认为HMAC SHA256(写为HS256);typ属性表示令牌的类型,JWT令牌统一写为JWT。最后,使用Base64 URL算法将上述JSON对象转换为字符串保存 。

1.2.4.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 算法转成字符串。

JWT只是适合在网络中传输一些非敏感的信息

1.2.4.3 Signature

Signature部分是对前两部分的签名,防止数据篡改。

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

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

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

1.2.5 JWT的使用方式

客户端收到服务器返回的JWT,可以储存在Cookie里面,也可以储存在 localStorage、SessionStorage

此后,客户端每次与服务器通信,都要带上这个JWT。你可以把它放在Cookie里面自动发送,但是这样不能跨域,所以更好的做法是放在HTTPs请求的头信息Authorization字段里面。

客户端收到服务器返回的JWT,可以储存在Cookie里面,也可以储存在 localStorage。SessionStorage

此后,客户端每次与服务器通信,都要带上这个JWT。你可以把它放在Cookie里面自动发送,但是这样不能跨域,所以更好的做法是放在HTTP请求的头信息Authorization字段里面。

步骤

1. 引入jar

 <!--引入jwt的依赖-->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>4.4.0</version>
        </dependency>

2. 创建jwt的工具类

  • 通过jwt创建token令牌

    java 复制代码
        private static  String key="layZhang";
    
        //创建token
        public static String createToken(Map<String,Object> map){
            //设置头部信息
            Map<String,Object> head=new HashMap<>();
            head.put("alg","HS256");
            head.put("typ","JWT");
            //设置发布日期
            Date date=new Date();
            //设置过期时间
            Calendar instance = Calendar.getInstance();//获取当前时间
            instance.set(Calendar.SECOND,7200);//在当前时间的基础上添加两个小时
            Date time = instance.getTime();//得到Date类型的时间
    
            //创建token
            String token = JWT.create()
                    .withHeader(head)//设置头部信息
                    .withIssuedAt(date)//设置发布时间
                    .withExpiresAt(time)//设置过期时间
                    .withClaim("userInfo", map)//设置个人信息
                    .sign(Algorithm.HMAC256(key));//签名
    
            return token;
        }
  • 校验token

    java 复制代码
     //校验token
        public static boolean verify(String token){
            Verification require = JWT.require(Algorithm.HMAC256(key));
            try {
                require.build().verify(token);
                return true;
            }catch (Exception e){
                System.out.println("token错误");
                return false;
            }
        }

    JWT.require()方法: 这是JWT库中的一个方法,用于创建一个Verification【验证】对象,该对象用于配置和执行JWT的验证过程。

    Algorithm.HMAC256(key): 这里指定了用于签名JWT的算法和密钥。HMAC256是一种基于哈希的消息认证码(HMAC)算法,它使用SHA-256哈希函数。key是一个密钥,用于生成和验证JWT的签名。这个密钥在生成JWT令牌和验证JWT令牌时必须相同。
    require.build(): 这个方法调用会基于之前通过JWT.require(...)方法配置的验证要求,构建一个JWTVerifier实例。这个实例包含了所有必要的验证配置(如签名算法和密钥)。

    .verify(token): 使用构建好的JWTVerifier实例来验证给定的token。如果token是有效的(即,它是由指定的密钥和算法签名的,且未被篡改),则此方法将成功执行。如果token无效,将抛出异常。
    综上所述,verify方法通过指定的密钥和算法验证给定的JWT令牌是否有效,并根据验证结果返回相应的布尔值。这种方法是Web应用中实现身份验证和授权的一种常见方式。

  • 根据token获取自定义的信息

    java 复制代码
    //根据token获取自定义的信息
        public static Map<String,Object> getInfo(String token,String mykey){
            JWTVerifier build = JWT.require(Algorithm.HMAC256(key)).build();
            Claim claim = build.verify(token).getClaim(mykey);
            return claim.asMap();
        }

    JWT.require(Algorithm.HMAC256(key)): 这部分代码与前面提到的验证token的方法类似,它创建了一个Verification配置,指定了用于验证JWT的算法(HMAC256)和密钥(key)。这里的key应该是一个在JWT生成和验证过程中都使用的共享密钥。

    .build(): 这个方法调用基于前面配置的验证要求,构建了一个JWTVerifier实例。这个实例将用于验证JWT令牌。
    build.verify(token): 使用构建的JWTVerifier实例来验证给定的token。如果token是有效的(即,它确实是由指定的密钥和算法签名的,并且没有被篡改),这个方法将返回一个DecodedJWT对象,该对象包含了JWT中的所有信息。

    .getClaim(mykey): 从验证并解码的JWT中获取与mykey键相关联的Claim对象。JWT中的信息以键值对的形式存储,其中每个键值对都是一个Claim。如果JWT中不存在与mykey对应的Claim,则此方法可能抛出异常或返回null(具体行为取决于JWT库的实现)。
    claim.asMap(): 如果Claim对象存在且不为空,这个方法将Claim中的信息转换为一个Map。这样,你就可以像操作普通Map一样方便地访问JWT中存储的自定义信息了

1.3 登录成功后返回json数据

AuthenticationSuccessHandler接口:只有一个抽象方法,为函数式接口,所以可以使用Lamda表达式重写抽象方法。这个接口是Spring Security用于处理成功认证后的行为的一个钩子。 用于自定义用户成功登录后的处理逻辑。当认证过程成功完成时(例如,用户提供了正确的用户名和密码),Spring Security会调用实现了这个接口的类的onAuthenticationSuccess方法。

java 复制代码
    private AuthenticationSuccessHandler successHandler(){
//        return new AuthenticationSuccessHandler() {
//            @Override  //
//            public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
//                //设置响应的编码
//                httpServletResponse.setContentType("application/json;charset=utf-8");
//                //获取输出对象
//                PrintWriter writer = httpServletResponse.getWriter();
//                //返回json数据即可
//                Map<String,Object> map=new HashMap<>();
//                map.put("username",authentication.getName());
//                Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
//                //获取权限
//                List<String> collect = authorities.stream().map(item -> item.getAuthority()).collect(Collectors.toList());
//
//                map.put("permissions",collect);
//                String token = JWTUtil.createToken(map);
//
//                //返回一个统一的json对象
//                R r=new R(200,"登录成功",token);
//                //转换为json字符串
//                String jsonString = JSON.toJSONString(r);
//                //servlet
//                writer.println(jsonString);
//                writer.flush();
//                writer.close();
//            }
//        };
        //使用Lambda表达式
        return (httpServletRequest, httpServletResponse, authentication) -> {
            //设置响应的编码
            httpServletResponse.setContentType("application/json;charset=utf-8");
            //获取输出对象
            PrintWriter writer = httpServletResponse.getWriter();
            //返回json数据即可
            Map<String,Object> map=new HashMap<>();
            map.put("username",authentication.getName());
            //获取权限信息列表
            Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
            //获取响应的权限标识符
            List<String> collect = authorities.stream().map(item -> item.getAuthority()).collect(Collectors.toList());

            map.put("permissions",collect);
            String token = JWTUtil.createToken(map);

            //返回一个统一的json对象
            R r=new R(200,"登录成功",token);
            //转换为json字符串
            String jsonString = JSON.toJSONString(r);
            //servlet
            writer.println(jsonString);
            writer.flush();
            writer.close();
        };
    }

修改配置

.successHandler(successHandler())

onAuthenticationSuccess方法:

  • 这是AuthenticationSuccessHandler接口中需要实现的方法。它有三个参数:HttpServletRequestHttpServletResponseAuthentication

  • HttpServletRequest: 提供了对当前HTTP请求信息的访问。

  • HttpServletResponse: 允许你控制对客户端的响应,比如设置响应头、发送响应体等。

  • Authentication: 包含了认证成功的用户的信息,比如用户名、密码(通常加密或散列)、权限等。
    设置响应编码和获取输出对象:

  • httpServletResponse.setContentType("application/json;charset=utf-8");: 设置响应的内容类型为JSON,并指定字符集为UTF-8。

  • PrintWriter writer = httpServletResponse.getWriter();: 获取一个PrintWriter对象,用于向客户端发送字符文本数据。
    构建返回的JSON数据:

  • 创建一个HashMap来存储要返回给客户端的数据。

  • Authentication对象中获取用户名并添加到map中。

  • 使用Java 8的流(Stream)从Authentication对象中获取用户的权限(GrantedAuthority),并将它们转换为字符串列表,然后添加到map中。
    获取权限标识符:

authorities 是一个 Collection 类型的集合,它包含了用户所拥有的权限信息。每个 GrantedAuthority 对象都代表了一个权限,通常是通过它的 getAuthority() 方法来获取权限的标识符(通常是一个字符串)。

使用 Java 8 的 Stream API,您可以将这个集合转换为一个新的 List,其中包含了所有权限的标识符。这是通过以下步骤实现的:

  1. 调用 stream() 方法 :将 Collection 转换为一个 Stream,这样您就可以使用 Stream API 提供的各种操作了。
  2. 调用 map() 方法map() 方法接受一个函数作为参数,这个函数会被应用到 Stream 中的每个元素上。在这个例子中,您传递了一个 lambda 表达式 item -> item.getAuthority(),它将每个 GrantedAuthority 对象映射为其权限标识符(即调用 getAuthority() 方法的结果)。这样,Stream 中的元素就从 GrantedAuthority 对象变成了字符串。
  3. 调用 collect() 方法collect() 方法是一个终端操作,它接受一个 Collector 来将 Stream 中的元素累积成一个结果。在这个例子中,您使用了 Collectors.toList() 来收集 Stream 中的所有元素到一个新的 List 中。

这行代码的作用就是:将用户所拥有的所有权限(GrantedAuthority 对象)转换为一个包含这些权限标识符(字符串)的列表。
构建统一的响应对象并转换为JSON字符串:

  • 创建一个R对象(假设这是一个自定义的响应类,用于封装响应的状态码、消息和数据),将状态码设置为200,消息设置为"登录成功!",并将JWT令牌作为数据设置进去。

  • 使用某个JSON库(如Fastjson、Jackson等)将R对象转换为JSON字符串。
    发送响应到客户端:

  • 使用PrintWriter将JSON字符串写入到HTTP响应中。

  • 调用flush()方法确保所有缓冲的输出都被发送到客户端。

  • 调用close()方法关闭PrintWriter
    总的来说,这个successHandler方法在用户成功登录后,会构建一个包含用户名、权限和JWT令牌的JSON响应,并将其发送给客户端。这样,客户端就可以使用这个JWT令牌进行后续的身份验证和授权操作。

1.4 登录失败返回的json数据

java 复制代码
//登录失败返回json数据
    private  AuthenticationFailureHandler  failureHandler(){
        return (httpServletRequest, httpServletResponse, e)->{
            //设置编码
            httpServletResponse.setContentType("application/json;charset=utf-8");
            //获取输出对象
            PrintWriter writer = httpServletResponse.getWriter();
            R r=new R(500,"登录失败!",e.getMessage());
            String s = JSON.toJSONString(r);
            writer.println(s);
            writer.flush();
            writer.close();
        };
    }

修改配置

.failureHandler(failureHandler()) 

1.5 权限不足返回的json数据

java 复制代码
 //权限不足返回json数据
    private AccessDeniedHandler  accessDeniedHandler(){
        return (httpServletRequest, httpServletResponse, e)->{
            httpServletResponse.setContentType("application/json;charset=utf-8");
            PrintWriter writer = httpServletResponse.getWriter();
            R r=new R(403,"权限不足,请联系管理员",e.getMessage());
            String s = JSON.toJSONString(r);
            writer.println(s);
            writer.flush();
            writer.close();
        };
    }

修改配置

//指定权限不足跳转的页面
        http.exceptionHandling().accessDeniedHandler(accessDeniedHandler());

1.6 未登录访问资源返回json数据

需要自定义一个过滤器

java 复制代码
@Component //交于spring容器管理
public class LoginFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        //若是登录路径,放行
        String requestURI = httpServletRequest.getRequestURI();
        String method = httpServletRequest.getMethod();
        if("/login".equals(requestURI)&&"POST".equals(method)){
            //放行
            filterChain.doFilter(httpServletRequest,httpServletResponse);
            return;
        }

        //统一编码格式
        httpServletResponse.setContentType("application/json;charset=utf-8");
        //1. 从请求头中获取token令牌
        String token = httpServletRequest.getHeader("token");
        //2. 判断token是否为null
        if(StringUtils.isEmpty(token)){
            //获取传输对象
            PrintWriter writer = httpServletResponse.getWriter();
            R r =new R(500,"未登录",null);
            String s = JSON.toJSONString(r);
            writer.write(s);
            writer.flush();
            writer.close();
            return;
        }
        //3. 验证token
     if(!JWTUtil.verify(token)){
         PrintWriter writer = httpServletResponse.getWriter();
         //返回一个token失效的json数据
         R r=new R(500,"token失效!",null);
         String s = JSON.toJSONString(r);
         writer.write(s);
         writer.flush();
         writer.close();
         return;
     }
     //把当前用户的信息封装到Authentication对象中
        SecurityContext context = SecurityContextHolder.getContext();
        Map<String, Object> userInfo = JWTUtil.getInfo(token, "userInfo");

        Object username = userInfo.get("username");
        //权限标识符
       // List<String> permissions = (List<String>) userInfo.get("permissions");
        List<String> permissions = (List<String>) userInfo.get("permission");

        //通过stream流转换类型
        List<SimpleGrantedAuthority> collect = permissions.stream().map(item -> new SimpleGrantedAuthority(item)).collect(Collectors.toList());
       // List<SimpleGrantedAuthority> collect = permissions.stream().map(item -> new SimpleGrantedAuthority(item)).collect(Collectors.toList());
        //三个参数
        //Object principal,账号
        //Object credentials,密码 null
        //Collection<? extends GrantedAuthority> authorities:权限
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken=new UsernamePasswordAuthenticationToken(username,null,collect);
        context.setAuthentication(usernamePasswordAuthenticationToken);

        //放行
        filterChain.doFilter(httpServletRequest,httpServletResponse);

    }
}

用于处理HTTP请求的过滤器方法,通常用于在请求到达控制器之前执行一些预处理操作
检查登录路径并放行

  • 首先,方法通过检查请求的URI和方法来确定是否是一个登录请求(通常是/login路径且方法为POST)。
  • 如果是登录请求,则直接调用filterChain.doFilter(httpServletRequest, httpServletResponse);来放行请求,允许它继续通过过滤器链到达相应的控制器。
    从请求头中获取Token

通过httpServletRequest.getHeader("token")从HTTP请求头中获取名为token的值,这个值通常是一个JWT(JSON Web Token),用于身份验证和授权。
检查Token是否为空

  • 使用StringUtils.isEmpty(token)(这里假设StringUtils是一个工具类,用于字符串操作)来检查Token是否为空或null。

  • 如果Token为空,则构造一个包含错误信息的JSON响应(状态码500,消息"未登录"),并写入响应体中,然后结束方法执行。
    验证Token:

  • 调用JWTUtil.verify(token)(这里假设JWTUtil是一个工具类,用于处理JWT)来验证Token的有效性。

  • 如果Token无效(例如,签名不匹配、过期等),则构造一个包含错误信息的JSON响应(状态码500,消息"token失效!"),并写入响应体中,然后结束方法执行。
    从Token中提取用户信息并封装

  • 使用JWTUtil.getInfo(token, "userInfo")(这里假设getInfo方法从Token中提取特定字段的信息,"userInfo"是字段名)从Token中提取用户信息。

  • 从用户信息中提取用户名和权限列表。注意,这里权限列表的键名从permissions更改为permission,这取决于Token中实际存储的键名。

  • 使用Java 8的Stream API将权限列表中的每个权限字符串转换为SimpleGrantedAuthority对象,这些对象代表了Spring Security中的权限。
    将用户信息封装到Authentication对象中

创建一个UsernamePasswordAuthenticationToken(或其他适合的Authentication子类)实例,设置用户名、密码(对于JWT通常不需要,但可以使用null或特殊值)、权限列表等,并将该实例设置到SecurityContextHolder
设置安全上下文

通过SecurityContextHolder.getContext().setAuthentication(...)将身份验证信息设置到当前线程的安全上下文中。这是必要的,因为Spring Security会在后续的处理过程中(如访问控制决策)检查这个上下文来确定当前用户的身份和权限。

修改配置类

在配置类中注入自定义的过滤器

在方法中将自定义的过滤器放在之前

java 复制代码
 //把自定义的过滤器放在UsernamePasswordAuthenticationFilter之前
        http.addFilterBefore(loginFilter, UsernamePasswordAuthenticationFilter.class);
相关推荐
G丶AEOM1 小时前
Redis与MySQL如何保证数据一致性
java·redis
北漂编程小王子1 小时前
maven <scope>compile</scope>作用
java·maven·compile标签作用
小手cool1 小时前
IDEA某个Impl下的引入的文件红色
java·intellij-idea
yours_Gabriel2 小时前
【微服务】认识微服务
java·微服务·架构
跳跳的向阳花2 小时前
03-06、SpringCloud第六章,升级篇,升级概述与Rest微服务案例构建
spring·spring cloud·微服务
ThetaarSofVenice2 小时前
【Java从入门到放弃 之 Java程序基础】
java·开发语言·python
夏子曦2 小时前
java——Tomcat调优策略
java·开发语言·tomcat
夏子曦2 小时前
java——利用 Tomcat 自定义的类加载器实现热加载
java·tomcat
G丶AEOM2 小时前
Redis中HGETALL和ZRANGE命令
java·redis
In 20292 小时前
矩阵【Lecode_HOT100】
java·算法·矩阵