SecurityJwt一个基于springSecurity的Json Web Token的实现 提要一、SpringSecurity- Spring Security,一种基于 Spring AOP 和 Servlet 过滤器的安全框架。它提供全面的安全性解决方案,同时在 Web 请求级和方法调用级处理身份确认和授权。。
- 来自Spring全家桶系列,与SpringBoot无缝衔接。为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
二、JSON Web TokenJSON Web Token(JWT)准确来说是一个规范。实际上就是一个字符串,它由三部分组成——头部(Header)、载荷(playload)与签名(signature)。 头部(Header)用于描述关于该JWT的最基本的信息,即该JWT本身的信息声明,如签名所用算法。 载荷(playload)是存放有效信息的地方。其中信息又分为三个部分——声明部分、公共部分(subject)、私有部分(claim)。 签证(signature)需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密构成(注意:secret是保存在服务器端的)。 在分布式中直接根据token取出保存的用户信息,以及对token可用性校验,单点登录更为简单。
三、开发环境介绍- Java版本:1.8
- 构建工具:Gradle(目前国内主流构建工具依然是Maven,但是笔者用过Gradle之后就不想再用Maven了,因为Gradle是真的方便很多。其仓库结构向下兼容Maven,也就是说可以使用任何Maven仓库。
build.gradle文件: plugins { id 'org.springframework.boot' version '2.2.0.RELEASE' id 'io.spring.dependency-management' version '1.0.8.RELEASE' id 'java'}group = 'org.zuoyu'version = '1.0.0'sourceCompatibility = '1.8'configurations { compileOnly { extendsFrom annotationProcessor }}repositories {// 这里使用的是阿里巴巴的Maven仓库 maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' }}dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' compileOnly 'org.projectlombok:lombok' runtimeOnly 'mysql:mysql-connector-java' annotationProcessor 'org.projectlombok:lombok'// jwt依赖 runtime('io.jsonwebtoken:jjwt-jackson:0.10.7') runtime('io.jsonwebtoken:jjwt-impl:0.10.7') compile('io.jsonwebtoken:jjwt-api:0.10.7') testImplementation('org.springframework.boot:spring-boot-starter-test') { exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' } testImplementation 'org.springframework.security:spring-security-test'}test { useJUnitPlatform()}四、源码说明在这里只分析关键代码(其中的JwtConstants.java是我自定义的final变量类)备注: 在security的配置文件中,将session管理器关闭,没有必要使用session。1. JwtTokenUtils.java(JWT的工具类)/** * 构建JWT * * @param subject - 实体 * @param authorities - 权限 * @param expiration - 保留时间 * @return - token */ private static String createJwt(String subject, String authorities, long expiration) { long nowMillis = System.currentTimeMillis(); return Jwts.builder() .setId(JwtConstants.createTokenId()) .signWith(SECRET_KEY, SignatureAlgorithm.HS256) .setIssuer(JwtConstants.JWT_ISSUER) .setSubject(subject) .claim(JwtConstants.ROLE_CLAIMS, authorities) .setIssuedAt(new Date(nowMillis)) .setNotBefore(new Date(nowMillis)) .setExpiration(new Date(nowMillis + expiration * 1000L)) .compact(); }在这里我们使用官方依赖包中的Jwts.builder()方法,创建一个token,其中—— - signWith就是设置私密钥与加密方式,SECRET_KEY为私密钥,SignatureAlgorithm.HS256为加密方式。
- setSubject为设置公共部分,该部分在客户端可解密。
- claim为设置私有部分,其参数为key—value形式。
- setIssuedAt为token的签发时间。
- setNotBefore为token的生效时间。
- setExpiration为token的失效时间。
解析token: /** * 解析token * * @param token - * @return - Claims */ private static Claims parseJwt(String token) { return Jwts.parser() .setSigningKey(SECRET_KEY) .parseClaimsJws(token) .getBody(); }在这里重点在与setSigningKey,传入我们在创建时候的私密钥SECRET_KEY。 还有几个与security方便交互的方法: /** * 根据账户构建token * * @param user - 账户 * @return - */ public static String createToken(User user, boolean isRememberMe) { long expiration = isRememberMe ? JwtConstants.EXPIRATION_REMEMBER : JwtConstants.EXPIRATION; String spacer = ","; List<string> authorities = Arrays.stream(user.getRoles().split(spacer)) .map(role -> "ROLE_" + role) .collect(Collectors.toList()); return createJwt(JsonUtil.beanToJsonString(user), JsonUtil.objectToJsonString(authorities), expiration); } /** * 获取用户 * * @param token - token * @return - User */ public static User getUserByToken(String token) { String subject = parseJwt(token).getSubject(); return JsonUtil.jsonStringToBean(subject, User.class); }/** * 获取用户的权限 * @param token - token * @return - 权限列表 */ public static Collection<!--? extends GrantedAuthority--> getAuthoritiesByToken(String token) { String roles = parseJwt(token).get(JwtConstants.ROLE_CLAIMS).toString(); return JsonUtil.jsonStringToCollection(roles, SimpleGrantedAuthority.class); }2. AuthenticationSuccessHandlerImpl.java(Security登录成功后的执行行为)/** * 登录成功的实现. * * @author zuoyu **/@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)public class AuthenticationSuccessHandlerImpl implements AuthenticationSuccessHandler { @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { String rememberMe = request.getParameter(JwtConstants.USER_LOGIN_REMEMBER_ME); boolean isRememberMe = Boolean.parseBoolean(rememberMe); User principal = (User) authentication.getPrincipal(); String token = JwtTokenUtils.createToken(principal, isRememberMe); response.setContentType("application/json;charset=utf-8"); response.setHeader(JwtConstants.TOKEN_HEADER, token); response.setStatus(HttpServletResponse.SC_OK); PrintWriter responseWriter = response.getWriter(); responseWriter.write("{\"message\":\"登录成功\"}"); responseWriter.flush(); responseWriter.close(); }}这段代码主要思路是——登录成功后,在authentication中获取已经认证成功的用户信息(user),然后将该user转换为token并返回给客户端。其中的isRememberMe是根据是否为true给予token不同的有效时间(查看完整源代码)。 3. JwtAuthorizationFilter.java(自定义基于JWT认证的过滤器)/** * JWT的权限过滤器. * * @author zuoyu * @program jwt * @create 2019-10-17 16:26 **/@Slf4jpublic class JwtAuthorizationFilter extends BasicAuthenticationFilter { public JwtAuthorizationFilter(AuthenticationManager authenticationManager) { super(authenticationManager); } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { String token = request.getHeader(JwtConstants.TOKEN_HEADER); if (StringUtils.isEmpty(token)) { chain.doFilter(request, response); return; } User user = JwtTokenUtils.getUserByToken(token); Collection<!--? extends GrantedAuthority--> authorities = JwtTokenUtils.getAuthoritiesByToken(token); UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken( user, null, authorities); SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken); super.doFilterInternal(request, response, chain); }}这段代码的从请求中获取token,并将从token中解析出用户信息(user)和权限信息(authorities)。并根据用户信息(user)和权限信息(authorities)创建属于security框架的权限身份(authentication),将其存入当前的security环境。 五、使用方法- 项目启动后自动建表(JPA)
- 注册(登录)的账户名称字段为userName
- 注册(登录)的账户密码字段为passWord
- 登录时的“记住我”字段为rememberMe
如何使用——登录之后,在响应Response的头Headers里有字段Authorization,该值就是Token。 后续的访问需在Request的头Headers内携带字段Authorization和其值,以此来表示身份。 rememberMe默认时效为一小时,为true时时效7天,设置路径在org.zuoyu.security.jwt.constants.JwtConstants.java。 测试路径查看org.zuoyu.security.jwt.controller.AuthController.java类。
|