ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

day76(Spring Security + JWT,JwtAuthenticationFilter)

2022-06-18 23:04:19  阅读:140  来源: 互联网

标签:Spring JWT System 过滤器 day76 println Security out


day76(Spring Security + JWT,JwtAuthenticationFilter)

Spring Security + JWT

1.将返回的数据改为JWT数据

2.JWT组成

1.Header(头):指定算法和当前数据类型

2.Payload(载荷):Claims(自定义数据)和过期时间

3.Signature(签名):算法和密钥

3.代码

@Service
public class AdminServiceImpl implements IAdminService {

   // ===== 原有其它代码 =====

    /**
     * JWT数据的密钥
     */
    private String secretKey = "fgfdsfadsfadsafdsafdsfadsfadsfdsafdasfdsafdsafdsafds4rttrefds";

    @Override
    public String login(AdminLoginDTO adminLoginDTO) {
        // ===== 原有其它代码 =====

        // 如果程序可以执行到此处,则表示登录成功
        // 生成此用户数据的JWT
        // Claims
    	User user=(User) authenticate.getPrincipal();
        System.out.println("从认证结果中获取Principal=" + user.getClass().getName());
        Map<String,Object> claims=new HshMap<>();
        claims.put("username",user.getUsername());
        claims.put("permissions",user.getAuthorities());
        System.out.println("即将向JWT中写入数据=" + claims);
        //JWT的组成部分:Header(头),Payload(载荷),Signature(签名)
        String jwt=Jwts.builder()
            //Header:指定算法与当前数据类型
            //格式:{"alg":算法,"typ":"jwt"}
            .setHeaderParam(Header.CONTENT_TYPE,"HS256")
            .setHeaderParam(Header.TYPE,Header.JWT_TYPE)
            //Payload:通常包含Claims(自定义数据)和过期时间
            .setClaims(claims)
            .setExpiration(new Data(System.currentTimeMillis()+5*60*1000))
            //Signature:由算法和密钥(secret key)两部分组成
            .signwith(SignatureAlgorithm.HS256,secretkey)
            //打包生成
            .compact();

        // 返回JWT数据
        return jwt;
    }

}

4.更改控制器处理请求的返回值的类型(AdminController)

1.原因:

因为在控制器中,应该响应的是JSON格式的数据,所以,需要在csmall-passport中添加依赖csmall-common

并将控制器处理请求的方法的返回值类型改为JsonResult<String>,并调整返回值:

2.代码:

//http://localhost:8080/admins/login?username=root&password=123456
@RequestMapping("/login")
public JsonResult<String> login(AdminLoginDTO adminLoginDTO){
    String jwt=adminService.login(adminLoginDTO);
    return JsonResult.ok(jwt);
}

3.结果:

{
    "state":20000,
    "message":null,
    "data":"eyJjdHkiOiJIUzI1NiIsInR5cCI6IkpXVCIsImFsZyI6IkhTMjU2In0.eyJwZXJtaXNzaW9ucyI6W3siYXV0aG9yaXR5IjoiL2Ftcy9hZG1pbi9kZWxldGUifSx7ImF1dGhvcml0eSI6Ii9hbXMvYWRtaW4vcmVhZCJ9LHsiYXV0aG9yaXR5IjoiL2Ftcy9hZG1pbi91cGRhdGUifSx7ImF1dGhvcml0eSI6Ii9wbXMvcHJvZHVjdC9kZWxldGUifSx7ImF1dGhvcml0eSI6Ii9wbXMvcHJvZHVjdC9yZWFkIn0seyJhdXRob3JpdHkiOiIvcG1zL3Byb2R1Y3QvdXBkYXRlIn1dLCJleHAiOjE2NTU0MzQwMzcsInVzZXJuYW1lIjoicm9vdCJ9.8ZIfpxxjJlwNo-E3JhXwH4sZR0J5-FU-HAOMu1Tg-44"
}

注意:以上只是访问/admin/login时会执行所编写的流程(发送用户名和密码,得到含JWT的结果),并不代表真正意义的实现了“登录”!

5.登录流程

  1. 客户端提交用户名和密码到服务器
  2. 服务器认证成功后响应JWT
  3. 客户端在后续的请求中都携带JWT
  4. 服务器端验证JWT来决定是否允许访问

6.添加Knife4j(便于体现客户端携带JWT)

且在使用Knife4j时,需要在白名单中添加相关资源路径

SecurityConfiguration

@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    // ===== 原有其它代码 =====

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // ===== 原有其它代码 =====

        // URL白名单
        String[] urls = {
                "/admins/login",
                "/doc.html",  // 从本行开始,以下是新增
                "/**/*.js",
                "/**/*.css",
                "/swagger-resources",
                "/v2/api-docs",
                "/favicon.ico"
        };

        // ===== 原有其它代码 =====
    }
}

7.在控制器添加测试访问的请求配置:便于测试(AdminController)

// 以下是测试访问的请求
@GetMapping("/hello")
public String sayHello() {
    return "hello~~~";
}

由于/admins/hello不在白名单中,直接访问会出现403错误

8.使用规范

  1. JWT数据必须携带在请求头(Request Header)的Authorization属性中
  2. 服务器端在每次接收到请求后
  3. 先判断请求头是否存在Authorization
  4. Authorization的值是否有效等操作

通过过滤器实现以上检查

9.过滤器

1.创建过滤类(JwtAuthenticationFilter)

csmall-passport的根包下的security包下创建JwtAuthenticationFilter过滤器类,需要继承自OncePerRequestFilter类:

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        System.out.println("JwtAuthenticationFilter.doFilterInternal()");
    }

}

2.将自定义过滤器注册在Spring Security的相关过滤器之前

原因:

  1. 所有的过滤器都必须注册后才可以使用,且同一个项目中允许存在多个过滤器,形成过滤器链

  2. 以上用于验证JWT的过滤器应该运行在Spring Security处理登录的过滤器之前,需要在自定义的SecurityConfiguration中的configure()方法中将以上自定义的过滤器注册在Spring Security的相关过滤器之前

  3. 代码

    @Configuration
    public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    
        // 新增
        @Autowired
        private JwtAuthenticationFilter jwtAuthenticationFilter;
        
        // ===== 原有其它代码 =====
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            // ===== 原有其它代码 =====
    
            // 注册处理JWT的过滤器
            // 此过滤器必须在Spring Security处理登录的过滤器之前
            http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
        }
    }
    

10.完成后表现

完成后,重启项目,无论对哪个路径发出请求,在控制台都可以看出输出了过滤器中的输出语句内容,并且,在浏览器将显示一片空白。

11.JwtAuthenticationFilter

需要实现:

  • 尝试从请求头中获取JWT数据
    • 如果无JWT数据,应该直接放行,Spring Security还会进行后续的处理,例如白名单的请求将允许访问,其它请求将禁止访问
    • 如果存在JWT数据,应该尝试解析
      • 如果解析失败,应该视为错误,可以要求客户端重新登录,客户端就可以得到新的、正确的JWT,客户端在下一次提交请求时,使用新的JWT即可正确访问
      • 将解析得到的数据封装到Authentication对象中
        • Spring Security的上下文中存储的数据类型是Authentication类型
        • 为避免存入1次后,Spring Security的上下文中始终存在Authentication,在此过滤器执行的第一时间,应该清除上下文中的数据
package cn.tedu.csmall.passport.security;

import cn.tedu.csmall.common.web.JsonResult;
import cn.tedu.csmall.common.web.State;
import com.alibaba.fastjson.JSON;
import io.jsonwebtoken.*;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
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.util.List;

/**
 * JWT过滤器:从请求头的Authorization中获取JWT中存入的用户信息
 * 并添加到Spring Security的上下文中
 * 以致于Spring Security后续的组件(包括过滤器等)能从上下文中获取此用户的信息
 * 从而验证是否已经登录、是否具有权限等
 */
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    /**
     * JWT数据的密钥
     */
    private String secretKey = "fgfdsfadsfadsafdsafdsfadsfadsfdsafdasfdsafdsafdsafds4rttrefds";

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        System.out.println("JwtAuthenticationFilter.doFilterInternal()");
        // 清除Spring Security上下文中的数据
        // 避免此前曾经存入过用户信息,后续即使没有携带JWT,在Spring Security仍保存有上下文数据(包括用户信息)
        System.out.println("清除Spring Security上下文中的数据");
        SecurityContextHolder.clearContext();
        // 客户端提交请求时,必须在请求头的Authorization中添加JWT数据,这是当前服务器程序的规定,客户端必须遵守
        // 尝试获取JWT数据
        String jwt = request.getHeader("Authorization");
        System.out.println("从请求头中获取到的JWT=" + jwt);
        // 判断是否不存在jwt数据
        if (!StringUtils.hasText(jwt)) {
            // 不存在jwt数据,则放行,后续还有其它过滤器及相关组件进行其它的处理,例如未登录则要求登录等
            // 此处不宜直接阻止运行,因为“登录”、“注册”等请求本应该没有jwt数据
            System.out.println("请求头中无JWT数据,当前过滤器将放行");
            filterChain.doFilter(request, response); // 继续执行过滤器链中后续的过滤器
            return; // 必须
        }

        // 注意:此时执行时,如果请求头中携带了Authentication,日志中将输出,且不会有任何响应,因为当前过滤器尚未放行
        // 以下代码有可能抛出异常的
        // TODO 密钥和各个Key应该统一定义
        String username = null;
        String permissionsString = null;
        try {
            System.out.println("请求头中包含JWT,准备解析此数据……");
            Claims claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();
            username = claims.get("username").toString();
            permissionsString = claims.get("permissions").toString();
            System.out.println("username=" + username);
            System.out.println("permissionsString=" + permissionsString);
        } catch (ExpiredJwtException e) {
            System.out.println("解析JWT失败,此JWT已过期:" + e.getMessage());
            JsonResult<Void> jsonResult = JsonResult.fail(
                    State.ERR_JWT_EXPIRED, "您的登录已过期,请重新登录!");
            String jsonString = JSON.toJSONString(jsonResult);
            System.out.println("响应结果:" + jsonString);
            response.setContentType("application/json; charset=utf-8");
            response.getWriter().println(jsonString);
            return;
        } catch (MalformedJwtException e) {
            System.out.println("解析JWT失败,此JWT数据错误,无法解析:" + e.getMessage());
            JsonResult<Void> jsonResult = JsonResult.fail(
                    State.ERR_JWT_MALFORMED, "获取登录信息失败,请重新登录!");
            String jsonString = JSON.toJSONString(jsonResult);
            System.out.println("响应结果:" + jsonString);
            response.setContentType("application/json; charset=utf-8");
            response.getWriter().println(jsonString);
            return;
        } catch (SignatureException e) {
            System.out.println("解析JWT失败,此JWT签名错误:" + e.getMessage());
            JsonResult<Void> jsonResult = JsonResult.fail(
                    State.ERR_JWT_SIGNATURE, "获取登录信息失败,请重新登录!");
            String jsonString = JSON.toJSONString(jsonResult);
            System.out.println("响应结果:" + jsonString);
            response.setContentType("application/json; charset=utf-8");
            response.getWriter().println(jsonString);
            return;
        } catch (Throwable e) {
            System.out.println("解析JWT失败,异常类型:" + e.getClass().getName());
            e.printStackTrace();
            JsonResult<Void> jsonResult = JsonResult.fail(
                    State.ERR_INTERNAL_SERVER_ERROR, "获取登录信息失败,请重新登录!");
            String jsonString = JSON.toJSONString(jsonResult);
            System.out.println("响应结果:" + jsonString);
            response.setContentType("application/json; charset=utf-8");
            response.getWriter().println(jsonString);
            return;
        }

        // 将此前从JWT中读取到的permissionsString(JSON字符串)转换成Collection<? extends GrantedAuthority>
        List<SimpleGrantedAuthority> permissions
                = JSON.parseArray(permissionsString, SimpleGrantedAuthority.class);
        System.out.println("从JWT中获取到的权限转换成Spring Security要求的类型:" + permissions);
        // 将解析得到的用户信息传递给Spring Security
        // 获取Spring Security的上下文,并将Authentication放到上下文中
        // 在Authentication中封装:用户名、null(密码)、权限列表
        // 因为接下来并不会处理认证,所以Authentication中不需要密码
        // 后续,Spring Security发现上下文中有Authentication时,就会视为已登录,甚至可以获取相关信息
        Authentication authentication
                = new UsernamePasswordAuthenticationToken(username, null, permissions);
        SecurityContextHolder.getContext().setAuthentication(authentication);
        System.out.println("将解析得到的用户信息传递给Spring Security");
        // 放行
        System.out.println("JwtAuthenticationFilter 放行");
        filterChain.doFilter(request, response);
    }

}

标签:Spring,JWT,System,过滤器,day76,println,Security,out
来源: https://www.cnblogs.com/xiaoyezilei/p/16389516.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有