Spring Boot+Spring Security+JWT 实现token验证

Spring Boot+Spring Security+JWT 实现token验证

  • 什么是JWT?
    • JWT的工作流程
    • JWT的主要应用场景
    • JWT的结构
  • SpringBoot+Spring Security和JWT的集成实现token验证
    • 引入JWT依赖
    • JWT的生成和解析工具类
    • Spring Security配置
    • 登录生成token
    • token的校验
    • 演示
    • 总结

通常情况下,把API直接暴露出去是风险很大的,不说别的,直接被机器攻击就喝一壶的。那么一般来说,对API要划分出一定的权限级别,然后做一个用户的鉴权,依据鉴权结果给予用户开放对应的API。目前,比较主流的方案有几种:

用户名和密码鉴权,使用Session保存用户鉴权结果。
使用OAuth进行鉴权(其实OAuth也是一种基于Token的鉴权,只是没有规定Token的生成方式)
自行采用Token进行鉴权
第一种就不介绍了,由于依赖Session来维护状态,也不太适合移动时代,新的项目就不要采用了。第二种OAuth的方案和JWT都是基于Token的,但OAuth其实对于不做开放平台的公司有些过于复杂。我们主要介绍第三种:JWT。

什么是JWT?

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

JWT的工作流程

下面是一个JWT的工作流程图。模拟一下实际的流程是这样的(假设受保护的API在/protected中)

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

JWT的主要应用场景

身份认证在这种场景下,一旦用户完成了登陆,在接下来的每个请求中包含JWT,可以用来验证用户身份以及对路由,服务和资源的访问权限进行验证。由于它的开销非常小,可以轻松的在不同域名的系统中传递,所有目前在单点登录(SSO)中比较广泛的使用了该技术。 信息交换在通信的双方之间使用JWT对数据进行编码是一种非常安全的方式,由于它的信息是经过签名的,可以确保发送者发送的信息是没有经过伪造的。

优点
1.简洁(Compact): 可以通过URL,POST参数或者在HTTP header发送,因为数据量小,传输速度也很快
2.自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库
3.因为Token是以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持。
4.不需要在服务端保存会话信息,特别适用于分布式微服务。

JWT的结构

JWT是由三段信息构成的,将这三段信息文本用.链接一起就构成了JWT字符串。
就像这样:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

JWT包含了三部分:
Header 头部(标题包含了令牌的元数据,并且包含签名和/或加密算法的类型)
Payload 负载 (类似于飞机上承载的物品)
Signature 签名/签证

Header
JWT的头部承载两部分信息:token类型和采用的加密算法。

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

声明类型:这里是jwt
声明加密的算法:通常直接使用 HMAC SHA256

加密算法是单向函数散列算法,常见的有MD5SHAHAMC
MD5(message-digest algorithm 5) (信息-摘要算法)缩写,广泛用于加密和解密技术,常用于文件校验。校验?不管文件多大,经过MD5后都能生成唯一的MD5值
SHA (Secure Hash Algorithm,安全散列算法),数字签名等密码学应用中重要的工具,安全性高于MD5
HMAC (Hash Message Authentication Code),散列消息鉴别码,基于密钥的Hash算法的认证协议。用公开函数和密钥产生一个固定长度的值作为认证标识,用这个标识鉴别消息的完整性。常用于接口签名验证

Payload
载荷就是存放有效信息的地方。
有效信息包含三个部分
1.标准中注册的声明
2.公共的声明
3.私有的声明

标准中注册的声明 (建议但不强制使用) :
iss: jwt签发者
sub: 面向的用户(jwt所面向的用户)
aud: 接收jwt的一方
exp: 过期时间戳(jwt的过期时间,这个过期时间必须要大于签发时间)
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

公共的声明 :
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.

私有的声明 :
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。

Signature
jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
header (base64后的)
payload (base64后的)
secret
这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。
密钥secret是保存在服务端的,服务端会根据这个密钥进行生成token和进行验证,所以需要保护好。

SpringBoot+Spring Security和JWT的集成实现token验证

引入JWT依赖

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>

JWT的生成和解析工具类

import io.jsonwebtoken.*;

import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
 * @author: xxm
 * @description:
 * @date: 2020/5/28 15:53
 */
public class JwtUtil {
    /**过期时间---24 hour*/
    private static final int EXPIRATION_TIME = 60*60*24;
    /**自己设定的秘钥*/
    private static final String SECRET = "023bdc63c3c5a4587*9ee6581508b9d03ad39a74fc0c9a9cce604743367c9646b";
    /**前缀*/
    public static final String TOKEN_PREFIX = "Bearer ";
    /**表头授权*/
    public static final String AUTHORIZATION = "Authorization";

    /**
     *
     * @author: xxm
     * 功能描述:创建Token
     * @date: 2020/5/28 16:09
     * @param: 
     * @return: 
     */
    public static String generateToken(String userName) {
        Calendar calendar = Calendar.getInstance();
        Date now = calendar.getTime();
        // 设置签发时间
        calendar.setTime(new Date());
        // 设置过期时间
        // 添加秒钟
        calendar.add(Calendar.SECOND, EXPIRATION_TIME);
        Date time = calendar.getTime();
        HashMap<String, Object> map = new HashMap<>();
        //you can put any data in the map
        map.put("userName", userName);
        String jwt = Jwts.builder()
                .setClaims(map)
                //签发时间
                .setIssuedAt(now)
                //过期时间
                .setExpiration(time)
                .signWith(SignatureAlgorithm.HS256, SECRET)
                .compact();
        //jwt前面一般都会加Bearer
        return TOKEN_PREFIX + jwt;
    }
    /**
     *
     * @author: xxm
     * 功能描述: 解密Token
     * @date: 2020/5/28 16:18
     * @param: 
     * @return: 
     */
    public static String validateToken(String token) {
        try {
            // parse the token.
            Map<String, Object> body = Jwts.parser()
                    .setSigningKey(SECRET)
                    .parseClaimsJws(token.replace(TOKEN_PREFIX, ""))
                    .getBody();
            String userName = body.get("userName").toString();
            return userName;
        }catch (ExpiredJwtException e) {
            throw e;
        } catch (UnsupportedJwtException e) {
            throw e;
        } catch (MalformedJwtException e) {
            throw e;
        } catch (SignatureException e) {
            throw e;
        } catch (IllegalArgumentException e) {
            throw e;
        } catch (Exception e){
            throw e;
        }
    }
}

Spring Security配置

Spring Security是一个基于Spring的通用安全框架,里面内容太多了,本文的主要目的也不是展开讲这个框架,而是如何利用Spring Security和JWT一起来完成API保护。所以关于Spring Secruity的基础内容或展开内容,请自行去官网学习(官网)。

import com.ggny.emep.interfac.springsecurityconfig.filter.JWTAuthenticationFilter;
import com.ggny.emep.interfac.springsecurityconfig.service.CustomUserServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;

import javax.sql.DataSource;

/**
 *
 * @author: xxm
 * 功能描述: SpringSecurity的配置
 * @date: 2020/5/28 15:14
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
    /**
     * 需要放行的URL
     */
    public static final String[] AUTH_WHITELIST = {
            "/user/login"
            // other public endpoints of your API may be appended to this array
    };
    @Autowired
    private DataSource dataSource;

    @Bean
    public JdbcTokenRepositoryImpl tokenRepository() {
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        tokenRepository.setDataSource(dataSource);
        //tokenRepository.setCreateTableOnStartup(true); // 启动创建表,创建成功后注释掉
        return tokenRepository;
    }

    @Bean
    UserDetailsService customUserService() { // 注册UserDetailsService 的bean
        return new CustomUserServiceImpl();
    }

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(customUserService()).passwordEncoder(new BCryptPasswordEncoder());
    }

    /**
     * 配置请求拦截
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and()
                //由于使用的是JWT,我们这里不需要csrf
                .csrf().disable()
                //基于token,所以不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                .authorizeRequests()
                //可以匿名访问的链接
                .antMatchers(AUTH_WHITELIST).permitAll()
                //其他所有请求需要身份认证
                .anyRequest().authenticated()

                .and()
                //.addFilter(new JWTLoginFilter(authenticationManager()))
                .addFilter(new JWTAuthenticationFilter(authenticationManager()));
    }

这是标准的SpringSecurity配置内容,就不在详细说明。注意其中的

.addFilter(new JwtAuthenticationFilter(authenticationManager()))

这行,将我们定义的JWT方法加入SpringSecurity的处理流程中。

重点就是配置请求拦截,由于我做的是一个接口服务,所以暂时除了登录验证,其他接口链接全部都要拦截,通过JWTAuthenticationFilter 过滤器来实现token的验证

登录生成token

验证用户名密码正确后,生成一个token,并将token返回给客户端

/**
     * 方法名:作用:登陆校验密码
     * 输入 username password  用户名,密码
     * 输出:code: 状态码   1 为认证成功 0 为用户不存在 -1 为密码不一致 -2 表示程序错误
     *       success:  true or false 执行成功或失败
     *       result:只在认证成功时返回,包含用户的全部信息
     *       messsage:
     *
     *
     */
    @ResponseBody
    @RequestMapping(value = "/login", method = RequestMethod.POST)
    public String toLogin(SysUser user) {
        JSONObject json=new JSONObject();
        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
        try {
            SysUser user1 = userControllerClient.getUserInfoByLoginName(user.getUsername());
            if (user1!=null) {
                String dbPassWord = user1.getPassword();
                if (bCryptPasswordEncoder.matches(user.getPassword(),dbPassWord)) {
                    //创建token

                    String token = JwtUtil.generateToken(user.getUsername());
                    json.put("success", true);
                    json.put("code", 1);
                    //json.put("result", user1);
                    json.put("time", DateUtil.dateToString(new Date()));
                    json.put("message", "登陆成功");
                    json.put(JwtUtil.AUTHORIZATION,token);
                } else {
                    json.put("success", false);
                    json.put("code", -1);
                    json.put("message", "登陆失败,密码错误");
                }
            }else {
                json.put("success", false);
                json.put("code", 0);
                json.put("message", "无此用户信息");
            }
        } catch (Exception e) {
            json.put("code", -2);
            json.put("success", false);
            json.put("message", e.getMessage());

        }

        return JSON.toJSONString(json);
    }

授权验证

用户一旦登录成功后,会拿到token,后续的请求都会带着这个token,服务端会验证token的合法性。

创建JWTAuthenticationFilter类,我们在这个类中实现token的校验功能。

token的校验

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.ggny.emep.interfac.common.JwtUtil;
import com.ggny.emep.interfac.springsecurityconfig.config.SpringSecurityConfig;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureException;
import io.jsonwebtoken.UnsupportedJwtException;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;

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.ArrayList;
import java.util.Arrays;

/**
 * token的校验
 * 该类继承自BasicAuthenticationFilter,在doFilterInternal方法中,
 * 从http头的Authorization 项读取token数据,然后用Jwts包提供的方法校验token的合法性。
 * 如果校验通过,就认为这是一个取得授权的合法请求
 * @author xxm
 */
public class JWTAuthenticationFilter extends BasicAuthenticationFilter {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        String url = request.getRequestURI();
        String header = request.getHeader(JwtUtil.AUTHORIZATION);

        JSONObject json=new JSONObject();
        //跳过不需要验证的路径
        if(null != SpringSecurityConfig.AUTH_WHITELIST&&Arrays.asList(SpringSecurityConfig.AUTH_WHITELIST).contains(url)){
            chain.doFilter(request, response);
            return;
        }
        if (StringUtils.isBlank(header) || !header.startsWith(JwtUtil.TOKEN_PREFIX)) {
            
            json.put("codeCheck", false);
            json.put("msg", "Token为空");
            response.setCharacterEncoding("UTF-8");
            response.getWriter().write(JSON.toJSONString(json));
            return;
        }
        try {
            UsernamePasswordAuthenticationToken authentication = getAuthentication(request,response);

            SecurityContextHolder.getContext().setAuthentication(authentication);
            chain.doFilter(request, response);

        }catch (ExpiredJwtException e) {
            //json.put("status", "-2");
            json.put("codeCheck", false);
            json.put("msg", "Token已过期");
            response.setCharacterEncoding("UTF-8");
            response.getWriter().write(JSON.toJSONString(json));
            logger.error("Token已过期: {} " + e);
        } catch (UnsupportedJwtException e) {
            //json.put("status", "-3");
            json.put("codeCheck", false);
            json.put("msg", "Token格式错误");
            response.setCharacterEncoding("UTF-8");
            response.getWriter().write(JSON.toJSONString(json));
            logger.error("Token格式错误: {} " + e);
        } catch (MalformedJwtException e) {
            //json.put("status", "-4");
            json.put("codeCheck", false);
            json.put("msg", "Token没有被正确构造");
            response.setCharacterEncoding("UTF-8");
            response.getWriter().write(JSON.toJSONString(json));
            logger.error("Token没有被正确构造: {} " + e);
        } catch (SignatureException e) {
            //json.put("status", "-5");
            json.put("codeCheck", false);
            json.put("msg", "Token签名失败");
            response.setCharacterEncoding("UTF-8");
            response.getWriter().write(JSON.toJSONString(json));
            logger.error("签名失败: {} " + e);
        } catch (IllegalArgumentException e) {
            //json.put("status", "-6");
            json.put("codeCheck", false);
            json.put("msg", "Token非法参数异常");
            response.setCharacterEncoding("UTF-8");
            response.getWriter().write(JSON.toJSONString(json));
            logger.error("非法参数异常: {} " + e);
        }catch (Exception e){
            //json.put("status", "-9");
            json.put("codeCheck", false);
            json.put("msg", "Invalid Token");
            response.setCharacterEncoding("UTF-8");
            response.getWriter().write(JSON.toJSONString(json));
            logger.error("Invalid Token " + e.getMessage());
        }
    }

    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request,HttpServletResponse response)  {
        String token = request.getHeader(JwtUtil.AUTHORIZATION);
        if (token != null) {
            String userName="";

            try {
                // 解密Token
                userName = JwtUtil.validateToken(token);
                if (StringUtils.isNotBlank(userName)) {
                    return new UsernamePasswordAuthenticationToken(userName, null, new ArrayList<>());
                }
            }catch (ExpiredJwtException e) {
                throw e;
                //throw new TokenException("Token已过期");
            } catch (UnsupportedJwtException e) {
                throw e;
                //throw new TokenException("Token格式错误");
            } catch (MalformedJwtException e) {
                throw e;
                //throw new TokenException("Token没有被正确构造");
            } catch (SignatureException e) {
                throw e;
                //throw new TokenException("签名失败");
            } catch (IllegalArgumentException e) {
                throw e;
                //throw new TokenException("非法参数异常");
            }catch (Exception e){
                throw e;
                //throw new IllegalStateException("Invalid Token. "+e.getMessage());
            }
            return null;
        }
        return null;
    }

}

该类继承自BasicAuthenticationFilter,在doFilterInternal方法中,从http头的Authorization 项读取token数据,然后用Jwts包提供的方法校验token的合法性。如果校验通过,就认为这是一个取得授权的合法请求。
这其中也包括了,token验证异常处理的返回信息

演示

  1. 当我们没有登录,也没有token时,是访问不了接口的
    Spring Boot+Spring Security+JWT 实现token验证_第2张图片
  2. 只有访问登录接口,并登陆成功,接口会返回给客户端token值
    Spring Boot+Spring Security+JWT 实现token验证_第3张图片
    3.客户端下次访问接口时,需要在表头中带着token值访问,才能通过验证,访问接口
    Spring Boot+Spring Security+JWT 实现token验证_第4张图片

总结

经过以上的配置,我们就完成了简单的访问权限控制和Token认证了,并添加了简单的异常处理,让我们的应用具有更好、更完善的错误提示和更安全的访问控制。

你可能感兴趣的:(token)