(转载)原文链接:https://my.oschina.net/10000000000/blog/3127268

1. 前言

欢迎阅读Spring Security 实战干货系列。之前我讲解了如何编写一个自己的 Jwt 生成器以及如何在用户认证通过后返回 Json Web Token 。今天我们来看看如何在请求中使用 Jwt 访问鉴权。DEMO 获取方法在文末。

2. 常用的 Http 认证方式

我们要在 Http 请求中使用 Jwt 我们就必须了解 常见的 Http 认证方式。

2.1 HTTP Basic Authentication

HTTP Basic Authentication 又叫基础认证,它简单地使用 Base64 算法对用户名、密码进行加密,并将加密后的信息放在请求头 Header 中,本质上还是明文传输用户名、密码,并不安全,所以最好在 Https 环境下使用。其认证流程如下:

客户端发起 GET 请求 服务端响应返回 401 Unauthorized, www-Authenticate 指定认证算法,realm 指定安全域。然后客户端一般会弹窗提示输入用户名称和密码,输入用户名密码后放入 Header 再次请求,服务端认证成功后以 200 状态码响应客户端。

2.2 HTTP Digest Authentication

为弥补 BASIC 认证存在的弱点就有了 HTTP Digest Authentication 。它又叫摘要认证。它使用随机数加上 MD5 算法来对用户名、密码进行摘要编码,流程类似 Http Basic Authentication ,但是更加复杂一些:

步骤1:跟基础认证一样,只不过返回带 WWW-Authenticate 首部字段的响应。该字段内包含质问响应方式认证所需要的临时咨询码(随机数,nonce)。 首部字段 WWW-Authenticate 内必须包含 realm 和 nonce 这两个字段的信息。客户端就是依靠向服务器回送这两个值进行认证的。nonce 是一种每次随返回的 401 响应生成的任意随机字符串。该字符串通常推荐由 Base64 编码的十六进制数的组成形式,但实际内容依赖服务器的具体实现

步骤2:接收到 401 状态码的客户端,返回的响应中包含 DIGEST 认证必须的首部字段 Authorization 信息。首部字段 Authorization 内必须包含 username、realm、nonce、uri 和 response 的字段信息,其中,realm 和 nonce 就是之前从服务器接收到的响应中的字段。

步骤3:接收到包含首部字段 Authorization 请求的服务器,会确认认证信息的正确性。认证通过后则会返回包含 Request-URI 资源的响应。

并且这时会在首部字段 Authorization-Info 写入一些认证成功的相关信息。

2.3 SSL 客户端认证

SSL 客户端认证就是通常我们说的 HTTPS 。安全级别较高,但需要承担 CA 证书费用。SSL 认证过程中涉及到一些重要的概念,数字证书机构的公钥、证书的私钥和公钥、非对称算法(配合证书的私钥和公钥使用)、对称密钥、对称算法(配合对称密钥使用)。相对复杂一些这里不过多讲述。

2.4 Form 表单认证

Form 表单的认证方式并不是HTTP规范。所以实现方式也呈现多样化,其实我们平常的扫码登录,手机验证码登录都属于表单登录的范畴。表单认证一般都会配合 Cookie,Session 的使用,现在很多 Web 站点都使用此认证方式。用户在登录页中填写用户名和密码,服务端认证通过后会将 sessionId 返回给浏览器端,浏览器会保存 sessionId 到浏览器的 Cookie 中。因为 HTTP 是无状态的,所以浏览器使用 Cookie 来保存 sessionId。下次客户端会在发送的请求中会携带 sessionId 值,服务端发现 sessionId 存在并以此为索引获取用户存在服务端的认证信息进行认证操作。认证过则会提供资源访问。

我们在Spring Security 实战干货:登录后返回 JWT Token 一文其实也是通过 Form 提交来获取 Jwt 其实 Jwt 跟 sessionId 同样的作用,只不过 Jwt 天然携带了用户的一些信息,而 sessionId 需要去进一步获取用户信息。

2.5 Json Web Token 的认证方式 Bearer Authentication

我们通过表单认证获取 Json Web Token ,那么如何使用它呢? 通常我们会把 Jwt 作为令牌使用 Bearer Authentication 方式使用。Bearer Authentication 是一种基于令牌的 HTTP 身份验证方案,用户向服务器请求访问受限资源时,会携带一个 Token 作为凭证,检验通过则可以访问特定的资源。最初是在 RFC 6750 中作为 OAuth 2.0 的一部分,但有时也可以单独使用。 我们在使用 Bear Token 的方法是在请求头的 Authorization 字段中放入 Bearer <token> 的格式的加密串(Json Web Token)。请注意 Bearer 前缀与 Token 之间有一个空字符位,与基本身份验证类似,Bearer Authentication 只能在HTTPS(SSL)上使用。

3. Spring Security 中实现接口 Jwt 认证

接下来我们是我们该系列的重头戏 ———— 接口的 Jwt 认证。

3.1 定义 Json Web Token 过滤器

无论上面提到的哪种认证方式,我们都可以使用 Spring Security 中的 Filter 来处理。 Spring Security 默认的基础配置没有提供对 Bearer Authentication 处理的过滤器, 但是提供了处理 Basic Authentication 的过滤器:

org.springframework.security.web.authentication.www.BasicAuthenticationFilter

BasicAuthenticationFilter 继承了 OncePerRequestFilter 。所以我们也模仿 BasicAuthenticationFilter 来实现自己的 JwtAuthenticationFilter 。 完整代码如下:

 package cn.felord.spring.security.filter;
import cn.felord.spring.security.exception.SimpleAuthenticationEntryPoint; import cn.felord.spring.security.jwt.JwtTokenGenerator; import cn.felord.spring.security.jwt.JwtTokenPair; import cn.felord.spring.security.jwt.JwtTokenStorage; import cn.hutool.json.JSONArray; import cn.hutool.json.JSONObject; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpHeaders; import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.CredentialsExpiredException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.User; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; 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; import java.util.Objects;
/**
 * jwt 认证拦截器 用于拦截 请求 提取jwt 认证
 *
 * @author dax
 * @since 2019/11/7 23:02
 */
@Slf4j public class JwtAuthenticationFilter extends OncePerRequestFilter {     private static final String AUTHENTICATION_PREFIX = "Bearer ";     /**
     * 认证如果失败由该端点进行响应
     */
    private AuthenticationEntryPoint authenticationEntryPoint = new SimpleAuthenticationEntryPoint();     private JwtTokenGenerator jwtTokenGenerator;     private JwtTokenStorage jwtTokenStorage;     public JwtAuthenticationFilter(JwtTokenGenerator jwtTokenGenerator, JwtTokenStorage jwtTokenStorage) {         this.jwtTokenGenerator = jwtTokenGenerator;         this.jwtTokenStorage = jwtTokenStorage;
    }     @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {         // 如果已经通过认证
        if (SecurityContextHolder.getContext().getAuthentication() != null) {
            chain.doFilter(request, response);             return;
        }         // 获取 header 解析出 jwt 并进行认证 无token 直接进入下一个过滤器  因为  SecurityContext 的缘故 如果无权限并不会放行
        String header = request.getHeader(HttpHeaders.AUTHORIZATION);         if (StringUtils.hasText(header) && header.startsWith(AUTHENTICATION_PREFIX)) {
            String jwtToken = header.replace(AUTHENTICATION_PREFIX, "");             if (StringUtils.hasText(jwtToken)) {                 try {
                    authenticationTokenHandle(jwtToken, request);
                } catch (AuthenticationException e) {
                    authenticationEntryPoint.commence(request, response, e);
                }
            } else {                 // 带安全头 没有带token
                authenticationEntryPoint.commence(request, response, new AuthenticationCredentialsNotFoundException("token is not found"));
            }         }
        chain.doFilter(request, response);
    }
    /**
     * 具体的认证方法  匿名访问不要携带token
     * 有些逻辑自己补充 这里只做基本功能的实现
     *
     * @param jwtToken jwt token
     * @param request  request
     */
    private void authenticationTokenHandle(String jwtToken, HttpServletRequest request) throws AuthenticationException {
        // 根据我的实现 有效token才会被解析出来
        JSONObject jsonObject = jwtTokenGenerator.decodeAndVerify(jwtToken);
        if (Objects.nonNull(jsonObject)) {
            String username = jsonObject.getStr("aud");
            // 从缓存获取 token
            JwtTokenPair jwtTokenPair = jwtTokenStorage.get(username);             if (Objects.isNull(jwtTokenPair)) {                 if (log.isDebugEnabled()) {
                    log.debug("token : {}  is  not in cache", jwtToken);
                }                 // 缓存中不存在就算 失败了
                throw new CredentialsExpiredException("token is not in cache");
            }
            String accessToken = jwtTokenPair.getAccessToken();
            if (jwtToken.equals(accessToken)) {                   // 解析 权限集合  这里
                JSONArray jsonArray = jsonObject.getJSONArray("roles");                 String roles = jsonArray.toString();                 List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList(roles);
                User user = new User(username, "[PROTECTED]", authorities);                 // 构建用户认证token
                UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(user, null, authorities);
                usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));                 // 放入安全上下文中
                SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
            } else {                 // token 不匹配
                if (log.isDebugEnabled()){
                    log.debug("token : {}  is  not in matched", jwtToken);
                }
                throw new BadCredentialsException("token is not matched");
            }
        } else {             if (log.isDebugEnabled()) {
                log.debug("token : {}  is  invalid", jwtToken);
            }             throw new BadCredentialsException("token is invalid");
        }
    }
}

具体看代码注释部分,逻辑有些地方根据你业务进行调整。匿名访问必然是不能带 Token 的!

3.2 配置 JwtAuthenticationFilter

首先将过滤器 JwtAuthenticationFilter 注入 Spring IoC 容器 ,然后一定要将 JwtAuthenticationFilter 顺序置于 UsernamePasswordAuthenticationFilter 之前:

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.csrf().disable()
                    .cors()
                    .and()                     // session 生成策略用无状态策略
                    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                    .and()
                    .exceptionHandling().accessDeniedHandler(new SimpleAccessDeniedHandler()).authenticationEntryPoint(new SimpleAuthenticationEntryPoint())
                    .and()
                    .authorizeRequests().anyRequest().authenticated()
                    .and()
                    .addFilterBefore(preLoginFilter, UsernamePasswordAuthenticationFilter.class)                     // jwt 必须配置于 UsernamePasswordAuthenticationFilter 之前
                    .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)                     // 登录  成功后返回jwt token  失败后返回 错误信息
                    .formLogin().loginProcessingUrl(LOGIN_PROCESSING_URL).successHandler(authenticationSuccessHandler).failureHandler(authenticationFailureHandler)
                    .and().logout().addLogoutHandler(new CustomLogoutHandler()).logoutSuccessHandler(new CustomLogoutSuccessHandler());         }

4. 使用 Jwt 进行请求验证

编写一个受限接口 ,我们这里是 http://localhost:8080/foo/test 。直接请求会被 401 。 我们通过下图方式获取 Token :

然后在 Postman 中使用 Jwt :

最终会认证成功并访问到资源。

5. 刷新 Jwt Token

我们在 Spring Security 实战干货:手把手教你实现JWT Token 中已经实现了 Json Web Token 都是成对出现的逻辑。accessToken 用来接口请求, refreshToken 用来刷新 accessToken 。我们可以同样定义一个 Filter 可参照 上面的 JwtAuthenticationFilter 。只不过 这次请求携带的是 refreshToken,我们在过滤器中拦截 URI跟我们定义的刷新端点进行匹配。同样验证 Token ,通过后像登录成功一样返回 Token 对即可。这里不再进行代码演示。

原文链接:https://my.oschina.net/10000000000/blog/3127268

6. 最后

如果文章对您有帮助,关注一下⬇️再走吧!

Spring Security 实战干货:使用 JWT 认证访问接口的更多相关文章

  1. Spring Security 实战干货:AuthenticationManager的初始化细节

    1. 前言 今天有个同学告诉我,在Security Learning项目的day11分支中出现了一个问题,验证码登录和其它登录不兼容了,出现了No Provider异常.还有这事?我赶紧跑了一遍还真是 ...

  2. Spring Security 实战干货: 简单的认识 OAuth2.0 协议

    1.前言 欢迎阅读 Spring Security 实战干货 系列文章 .OAuth2.0 是近几年比较流行的授权机制,对于普通用户来说可能每天你都在用它,我们经常使用的第三方登录大都基于 OAuth ...

  3. Spring Security 实战干货:实现自定义退出登录

    文章目录 1. 前言 2. 我们使用 Spring Security 登录后都做了什么 2. 退出登录需要我们做什么 3. Spring Security 中的退出登录 3.1 LogoutFilte ...

  4. Spring Security 实战干货:如何实现不同的接口不同的安全策略

    1. 前言 欢迎阅读 Spring Security 实战干货 系列文章 .最近有开发小伙伴提了一个有趣的问题.他正在做一个项目,涉及两种风格,一种是给小程序出接口,安全上使用无状态的JWT Toke ...

  5. Spring Security 实战干货:图解Spring Security中的Servlet过滤器体系

    1. 前言 我在Spring Security 实战干货:内置 Filter 全解析对Spring Security的内置过滤器进行了罗列,但是Spring Security真正的过滤器体系才是我们了 ...

  6. Spring Security 实战干货:理解AuthenticationManager

    1. 前言 我们上一篇介绍了UsernamePasswordAuthenticationFilter的工作流程,留下了一个小小的伏笔,作为一个Servlet Filter应该存在一个doFilter实 ...

  7. Spring Security 实战干货:图解用户是如何登录的

    1. 前言 欢迎阅读Spring Security 实战干货系列文章,在集成Spring Security安全框架的时候我们最先处理的可能就是根据我们项目的实际需要来定制注册登录了,尤其是Http登录 ...

  8. Spring Security 实战干货:OAuth2第三方授权初体验

    1. 前言 Spring Security实战干货系列 现在很多项目都有第三方登录或者第三方授权的需求,而最成熟的方案就是OAuth2.0授权协议.Spring Security也整合了OAuth2. ...

  9. Spring Security 实战干货:客户端OAuth2授权请求的入口

    1. 前言 在Spring Security 实战干货:OAuth2第三方授权初体验一文中我先对OAuth2.0涉及的一些常用概念进行介绍,然后直接通过一个DEMO来让大家切身感受了OAuth2.0第 ...

随机推荐

  1. js常用但是不容易记住的代码

    <!-- iframe 自适应高度度 --><iframe src="__CONTROLLER__/showlist" frameborder="0&q ...

  2. glibc编译安装

    glibc是gnu发布的libc库,即c运行库.glibc是linux系统中最底层的api,几乎其它任何运行库都会依赖于glibc.glibc除了封装linux操作系统所提供的系统服务外,它本身也提供 ...

  3. Java开发桌面程序学习(二)————fxml布局与控件学习

    JavaFx项目 新建完项目,我们的项目有三个文件 Main.java 程序入口类,载入界面并显示 Controller.java 事件处理,与fxml绑定 Sample.fxml 界面 sample ...

  4. WPF DataGrid显示MySQL查询信息,且可删除、修改、插入 (原发布 csdn 2018-10-13 20:07:28)

    1.入行好几年了,工作中使用数据库几率很小(传统行业).借着十一假期回家机会,学习下数据库. 2.初次了解数据库相关知识,如果本文有误,还望告知. 3.本文主要目的,记录下wpf界面显示数据库信息,且 ...

  5. VS2017安装使用Easyx时出现的问题及解决方法

    EasyX 是针对 Visual C++ 的绘图库,在初学 C 语言实现图形和游戏编程.图形学.分形学等需要绘图实践的领域有一定应用. EasyX 库在 Visual C++ 中模拟了 Turbo C ...

  6. Web前端基础(3):HTML(三)

    1. body中的相关标签 1.1 表格标签:table.tr.td HTML表格由<table>标签以及一个或多个<tr>.<th>或<td>标签组成 ...

  7. Latex学习笔记 第一章

    1.使用空行分段. 空行只起分段的作用,使用过多的空行并不起增大段间间距的作用. 2.段前不用打空格,LateX会自动完成文字的缩进. 即使打了也会被自动忽略. 3.通常汉字后面的空格会被忽略,其他符 ...

  8. zTree插件的应用

    需要用到的js和css文件 <link rel="stylesheet" href="__PUBLIC__/zTree/css/demo.css" typ ...

  9. oracle 利用序列与触发器实现列自增

    实现步骤:先创建序列,后创建触发器 1.创建序列 create sequence 序列名 increment start maxvalue ; 2.创建触发器 create or replace tr ...

  10. 自定义栈Stack 和 队列Queue

    自定义栈 接口 package com.test.custom; public interface IStack<E> { E pop(); void push(E e); E peek( ...