• SpringSecurity 整合 JWT


    项目集成Spring Security(一)

    在上一篇基础上继续集成 JWT ,实现用户身份验证。

    前言

    前后端分离项目中,如果直接把 API 接口对外开放,我们知道这样风险是很大的,所以在上一篇中我们引入了 Spring Security ,但是我们在登陆后缺少了请求凭证部分。

    什么是JWT?

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

    JWT的工作流程

    1、用户进入登录页,输入用户名、密码,进行登录;
    2、服务器验证登录鉴权,如果改用户合法,根据用户的信息和服务器的规则生成 JWT Token
    3、服务器将该 token 以 json 形式返回(不一定要json形式,这里说的是一种常见的做法)
    4、用户得到 token,存在 localStorage、cookie 或其它数据存储形式中。以后用户请求 /protected 中的 API 时,在请求的 header 中加入 Authorization: Bearer xxxx(token)。此处注意token之前有一个7字符长度的 Bearer。
    5、服务器端对此 token 进行检验,如果合法就解析其中内容,根据其拥有的权限和自己的业务逻辑给出对应的响应结果。
    6、用户取得结果

    如下如所示:

    7790cc3aade467c985e2e4a8105b89f1.png7790cc3aade467c985e2e4a8105b89f1.png

    来看一下 JWT:

    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

    token 分成了三部分,头部(header),荷载(Payload) 和 签名(Signature),每部分用 . 分隔,其中头部和荷载使用了base64编码,分别解码之后得到两个JSON串:

    第一部分-头部:

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

    alg字段为加密算法,这是告诉我们 HMAC 采用 HS512 算法对 JWT 进行的签名。

    第二部分-荷载:

    {
      "sub""1234567890",
      "name""John Doe",
      "iat"1516239022
    }

    荷载的字段及含义:

    • iss: 该JWT的签发者
    • sub: 该JWT所面向的用户
    • aud: 接收该JWT的一方
    • exp(expires): 什么时候过期,这里是一个Unix时间戳
    • iat(issued at): 在什么时候签发的

    这段告诉我们这个Token中含有的数据声明(Claim),这个例子里面有三个声明:sub, name 和 iat。在我们这个例子中,分别代表着
    所面向的用户、用户名、创建时间,当然你可以把任意数据声明在这里。

    第三部分-签名:

    第三部分签名则不能使用base64解码出来,该部分用于验证头部和荷载数据的完整性。

    JWT的生成和解析

    引入依赖:

    <!-- JWT -->
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>0.9.1</version>
    </dependency>

    创建一个测试类尝试一下 JWT 的生成:

    public class Test {

        public static void main(String[] args){
            String token = Jwts.builder()
                    主题 放入用户名
                    .setSubject("niceyoo")
                    自定义属性 放入用户拥有请求权限
                    .claim("authorities","admin")
                    失效时间
                    .setExpiration(new Date(System.currentTimeMillis() + 7 * 60 * 1000))
                    签名算法和密钥
                    .signWith(SignatureAlgorithm.HS512, "tmax")
                    .compact();
            System.out.println(token);
        }

    }

    控制台打印如下:

    eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJuaWNleW9vIiwiYXV0aG9yaXRpZXMiOiJhZG1pbiIsImV4cCI6MTU1OTQ1ODM1M30.keCiHrcEr0IWXfZLocgHS8znn7uSiaZW1IT6bTs-EQG0NPsb6-Aw_XbGQea4mez2CcAflgMqtzIpsDjZsUOVug

    数据声明(Claim)是一个自定义属性,可以用来放入用户拥有请求权限。上边为简单直接传了一个 'admin'。

    再看看解析:

    public static void main(String[] args){

        try {
            解析token
            Claims claims = Jwts.parser()
                    .setSigningKey("tmax")
                    .parseClaimsJws("eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJuaWNleW9vIiwiYXV0aG9yaXRpZXMiOiJhZG1pbiIsImV4cCI6MTU1OTQ1OTc2Mn0.MkSJtGaVePLa-eM3gylh1T3fwODg-6ceDDOxscXAQKun-qNrbQFcKPNqXhblbXPNLhaJyEnwugNANCTs98UNmA")
                    .getBody();

            System.out.println(claims);
            获取用户名
            String username = claims.getSubject();
            System.out.println("username:"+username);
            获取权限
            String authority = claims.get("authorities").toString();
            System.out.println("权限:"+authority);
        } catch (ExpiredJwtException e) {
            System.out.println("jwt异常");
        } catch (Exception e){
            System.out.println("异常");
        }
    }

    控制台打印:

    {sub=niceyoo, authorities=admin, exp=1559459762}
    username:niceyoo
    权限:admin

    JWT 本身没啥难度,但安全整体是一个比较复杂的事情,JWT 只不过提供了一种基于 token 的请求验证机制。但我们的用户权限,对于 API 的权限划分、资源的权限划分,用户的验证等等都不是JWT负责的。也就是说,请求验证后,你是否有权限看对应的内容是由你的用户角色决定的。所接下来才是我们的重点,Spring Security 整合 JWT。

    集成JWT

    要想要 JW T在 Spring 中工作,我们应该新建一个 JWT filter,并把它配置在 WebSecurityConfig 中。

    WebSecurityConfigurerAdapter.java

    @Slf4j
    @Configuration
    @EnableGlobalMethodSecurity(prePostEnabled=true)
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

        @Autowired
        private UserDetailsServiceImpl userDetailsService;

        @Autowired
        private AuthenticationSuccessHandler successHandler;

        @Autowired
        private AuthenticationFailHandler failHandler;

        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());加密
        }

        @Override
        protected void configure(HttpSecurity http) throws Exception {

            ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http
                    .authorizeRequests();

            registry.and()
                表单登录方式
                .formLogin()
                .permitAll()
                成功处理类
                .successHandler(successHandler)
                失败
                .failureHandler(failHandler)
                .and()
                .logout()
                .permitAll()
                .and()
                .authorizeRequests()
                任何请求
                .anyRequest()
                需要身份认证
                .authenticated()
                .and()
                关闭跨站请求防护
                .csrf().disable()
                前后端分离采用JWT 不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                添加JWT过滤器 除已配置的其它请求都需经过此过滤器
                .addFilter(new JWTAuthenticationFilter(authenticationManager(), 7));
        }
    }

    相较于上一篇主要多了如下一行配置:

    .addFilter(new JWTAuthenticationFilter(authenticationManager(), 7));

    JWTAuthenticationFilter.java

    @Slf4j
    public class JWTAuthenticationFilter extends BasicAuthenticationFilter   {

        private Integer tokenExpireTime;

        public JWTAuthenticationFilter(AuthenticationManager authenticationManager, Integer tokenExpireTime) {
            super(authenticationManager);
            this.tokenExpireTime = tokenExpireTime;
        }

        public JWTAuthenticationFilter(AuthenticationManager authenticationManager, AuthenticationEntryPoint authenticationEntryPoint) {
            super(authenticationManager, authenticationEntryPoint);
        }

        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {

            String header = request.getHeader(SecurityConstant.HEADER);
            if(StrUtil.isBlank(header)){
                header = request.getParameter(SecurityConstant.HEADER);
            }
            Boolean notValid = StrUtil.isBlank(header) || (!header.startsWith(SecurityConstant.TOKEN_SPLIT));
            if (notValid) {
                chain.doFilter(request, response);
                return;
            }
            try {
                UsernamePasswordAuthenticationToken 继承 AbstractAuthenticationToken 实现 Authentication
                所以当在页面中输入用户名和密码之后首先会进入到 UsernamePasswordAuthenticationToken验证(Authentication),
                UsernamePasswordAuthenticationToken authentication = getAuthentication(header, response);
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }catch (Exception e){
                e.toString();
            }

            chain.doFilter(request, response);
        }

        private UsernamePasswordAuthenticationToken getAuthentication(String header, HttpServletResponse response) {

            用户名
            String username = null;
            权限
            List<GrantedAuthority> authorities = new ArrayList<>();


            try {
                解析token
                Claims claims = Jwts.parser()
                        .setSigningKey(SecurityConstant.JWT_SIGN_KEY)
                        .parseClaimsJws(header.replace(SecurityConstant.TOKEN_SPLIT, ""))
                        .getBody();
                logger.info("claims:"+claims);
                获取用户名
                username = claims.getSubject();
                logger.info("username:"+username);
                获取权限
                String authority = claims.get(SecurityConstant.AUTHORITIES).toString();
                logger.info("authority:"+authority);
                if(!StringUtils.isEmpty(authority)){
                    authorities.add(new SimpleGrantedAuthority(authority));
                }

            } catch (ExpiredJwtException e) {
                ResponseUtil.out(response, ResponseUtil.resultMap(false,401,"登录已失效,请重新登录"));
            } catch (Exception e){
                log.error(e.toString());
                ResponseUtil.out(response, ResponseUtil.resultMap(false,500,"解析token错误"));
            }

            if(StrUtil.isNotBlank(username)) {
                踩坑提醒 此处password不能为null
                User principal = new User(username, "", authorities);
                return new UsernamePasswordAuthenticationToken(principal, null, authorities);
            }
            return null;
        }
    }

    接下来我们启动项目看看:

    访问项目中已有的链接:

    http://localhost:7777/tmax/videoCategory/getAll

    老样子认证一波:

    其中 niceyoo、 为数据库用户信息

    登陆成功后获取返回的 token,注意,此 token 是由 JWT 生成的:

     String token = SecurityConstant.TOKEN_SPLIT + Jwts.builder()
                主题 放入用户名
                .setSubject(username)
                自定义属性 放入用户拥有请求权限
                .claim(SecurityConstant.AUTHORITIES, authorities)
                失效时间
                .setExpiration(new Date(System.currentTimeMillis() + 7 * 60 * 1000))
                签名算法和密钥
                .signWith(SignatureAlgorithm.HS512, SecurityConstant.JWT_SIGN_KEY)
                .compact();

    浏览器返回 token 如下:

    ad45accf0b31c606a10c568acbddec19.pngad45accf0b31c606a10c568acbddec19.png

    然后我们通过 token 凭证去访问上边的方法:

    e3c05b207e1ec11e1f23560ef1b724b6.pnge3c05b207e1ec11e1f23560ef1b724b6.png

    后台打印信息:

    claims:{sub=niceyoo, authorities=admin, exp=1559472866}
    username:niceyoo
    authority:admin

    随便改一下 token ,返回如下:

    172cbf88bbb16a6679931885c2bdd2c4.png
     
    18年专科毕业后,期间一度迷茫,最近我创建了一个公众号用来记录自己的成长。
  • 相关阅读:
    在vs2010里使用EF4.3的Code First个人使用笔记
    如何充分利用C#匿名方法的平台优势(转)
    打开Microsoft SQL Server Management Studio 2005非常慢,特别慢的原因
    PowerShell msbuild
    很久没写了
    第一篇博客,逗大家一笑
    以编程方式调用按钮1(button1)的 Click 事件
    VS2010删除已安装的联机模板
    创建和分享你的Visual Studio color
    异步获取CMD命令行输出内容
  • 原文地址:https://www.cnblogs.com/niceyoo/p/10964277.html
Copyright © 2020-2023  润新知