SpringBoot整合SpringSecurity和JWT实现mymes认证和授权(一)
本文主要讲解mymes相同通过SpringBoot整合SpringSecurity和JWT来实现后台用户的授权和登录功能,因为这部分比较重要,讲解分3-4次讲完,分别介绍SpringSecurity和JWT,以及动态管理权限。
SpringSecurity简介
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
JWT简介
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景 。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
JWT的组成
第一部分我们称它为头部(header),第二部分我们称其为载荷(payload, 类似于飞机上承载的物品),第三部分是签证(signature).JWT token的格式:header.payload.signature
1.header
JWT的头部承载两部分信息
- 1.声明类型,这里是JWT
- 2.声明加密算法,通常直接使用HMAC SHA256 完整的头部信息如下:
{ 'typ': 'JWT', 'alg': 'HS256'}
2.playload
载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分
- 标准中注册的声明
- 公共的声明
- 私有的声明 ==标准==中注册的声明 (==建议但不强制使用==) :
iss: jwt签发者sub: jwt所面向的用户aud: 接收jwt的一方exp: jwt的过期时间,这个过期时间必须要大于签发时间nbf: 定义在什么时间之前,该jwt都是不可用的.iat: jwt的签发时间jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
定义一个payload:
{ "sub": "admin", "name": "John Doe", "created":1489079981499, "exp":1489681894}
1.修改SpringBoot配置文件
在application.yml中添加数据源配置和MyBatis的mapper.xml路径配置
server: port: 9999spring: application: name: mes-demo datasource: url: jdbc:mysql://localhost:3306/mymes?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai username: reader password: 123456mybatis: mapper-locations: - classpath:mapper/*.xml - classpath*:com/**/mapper/*.xml
在application.properties中添加数据源配置
jdbc.driverClass=com.mysql.jdbc.Driverjdbc.connectionURL=jdbc:mysql://localhost:3306/mymes?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghaijdbc.userId=readerjdbc.password=123456
signature
jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
- header (base64后的)
- payload (base64后的)
- secret signature为以header和payload生成的签名,一旦header和payload被篡改,验证将失败
String sig = HMACSHA512(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)
JWT分析
下面是一个JWT字符串:
eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiY3JlYXRlZCI6MTQ4OTA3OTk4MTQ5OSwiZXhwIjoxNDg5NjgxODk0LCJpYXQiOjE1MTYyMzkwMjJ9.DP9GRZAZPX1c8B16FhaiudBVi7czyMOqgFnSVIjEnd0C4NZdONi4tPpx6PBUBVih-zfiT1X8ATykXspmj_CS2A
可以通过JWT官方网站进行解析 https://jwt.io/
JWT实现项目认证以及授权的方式:
用户登录,获取token,将token添加到http的header中一个叫Authorization的头中,后台程序通过对头信息的解码和数字签名获取用户信息,用来实现认证授权
SrpringSecurity和JWT
在pom.xml中添加项目依赖
org.springframework.boot spring-boot-starter-security cn.hutool hutool-all 4.5.7 io.jsonwebtoken jjwt 0.9.0
添加JWT通用类
用于生成解析JWT
package com.cn.mymes.utils;import cn.hutool.core.date.DateUtil;import cn.hutool.core.util.StrUtil;import io.jsonwebtoken.Claims;import io.jsonwebtoken.Jwts;import io.jsonwebtoken.SignatureAlgorithm;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Value;import org.springframework.security.core.userdetails.UserDetails;import java.util.Date;import java.util.HashMap;import java.util.Map;/** * JwtToken生成的工具类 * JWT token的格式:header.payload.signature * header的格式(算法、token的类型): * {"alg": "HS512","typ": "JWT"} * payload的格式(用户名、创建时间、生成时间): * {"sub":"wang","created":1489079981393,"exp":1489684781} * signature的生成算法: * HMACSHA512(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret) * Created by macro on 2018/4/26. */public class JwtTokenUtil { private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenUtil.class); private static final String CLAIM_KEY_USERNAME = "sub"; private static final String CLAIM_KEY_CREATED = "created"; @Value("${jwt.secret}") private String secret; @Value("${jwt.expiration}") private Long expiration; @Value("${jwt.tokenHead}") private String tokenHead; /** * 根据负责生成JWT的token */ private String generateToken(Map claims) { return Jwts.builder() .setClaims(claims) .setExpiration(generateExpirationDate()) .signWith(SignatureAlgorithm.HS512, secret) .compact(); } /** * 从token中获取JWT中的负载 */ private Claims getClaimsFromToken(String token) { Claims claims = null; try { claims = Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); } catch (Exception e) { LOGGER.info("JWT格式验证失败:{}", token); } return claims; } /** * 生成token的过期时间 */ private Date generateExpirationDate() { return new Date(System.currentTimeMillis() + expiration * 1000); } /** * 从token中获取登录用户名 */ public String getUserNameFromToken(String token) { String username; try { Claims claims = getClaimsFromToken(token); username = claims.getSubject(); } catch (Exception e) { username = null; } return username; } /** * 验证token是否还有效 * * @param token 客户端传入的token * @param userDetails 从数据库中查询出来的用户信息 */ public boolean validateToken(String token, UserDetails userDetails) { String username = getUserNameFromToken(token); return username.equals(userDetails.getUsername()) && !isTokenExpired(token); } /** * 判断token是否已经失效 */ private boolean isTokenExpired(String token) { Date expiredDate = getExpiredDateFromToken(token); return expiredDate.before(new Date()); } /** * 从token中获取过期时间 */ private Date getExpiredDateFromToken(String token) { Claims claims = getClaimsFromToken(token); return claims.getExpiration(); } /** * 根据用户信息生成token */ public String generateToken(UserDetails userDetails) { Map claims = new HashMap<>(); claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername()); claims.put(CLAIM_KEY_CREATED, new Date()); return generateToken(claims); } /** * 当原来的token没过期时是可以刷新的 * * @param oldToken 带tokenHead的token */ public String refreshHeadToken(String oldToken) { if(StrUtil.isEmpty(oldToken)){ return null; } String token = oldToken.substring(tokenHead.length()); if(StrUtil.isEmpty(token)){ return null; } //token校验不通过 Claims claims = getClaimsFromToken(token); if(claims==null){ return null; } //如果token已经过期,不支持刷新 if(isTokenExpired(token)){ return null; } //如果token在30分钟之内刚刷新过,返回原token if(tokenRefreshJustBefore(token,30*60)){ return token; }else{ claims.put(CLAIM_KEY_CREATED, new Date()); return generateToken(claims); } } /** * 判断token在指定时间内是否刚刚刷新过 * @param token 原token * @param time 指定时间(秒) */ private boolean tokenRefreshJustBefore(String token, int time) { Claims claims = getClaimsFromToken(token); Date created = claims.get(CLAIM_KEY_CREATED, Date.class); Date refreshDate = new Date(); //刷新时间在创建时间的指定时间内 if(refreshDate.after(created)&&refreshDate.before(DateUtil.offsetSecond(created,time))){ return true; } return false; }}
添加SpringSecurity项目配置类
package com.cn.mymes.utils.config;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;import org.springframework.context.annotation.Bean;import org.springframework.http.HttpMethod;import org.springframework.security.authentication.AuthenticationManager;import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;import org.springframework.security.config.annotation.web.builders.HttpSecurity;import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;import org.springframework.security.config.http.SessionCreationPolicy;import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;import org.springframework.security.crypto.password.PasswordEncoder;import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;/** * 对SpringSecurity的配置的扩展,支持自定义白名单资源路径和查询用户逻辑 * */public class SecurityConfigBase extends WebSecurityConfigurerAdapter { @Autowired(required = false) private DynamicSecurityService dynamicSecurityService; @Override protected void configure(HttpSecurity httpSecurity) throws Exception { ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry registry = httpSecurity .authorizeRequests(); //不需要保护的资源路径允许访问 for (String url : ignoreUrlsConfig().getUrls()) { registry.antMatchers(url).permitAll(); } //允许跨域请求的OPTIONS请求 registry.antMatchers(HttpMethod.OPTIONS) .permitAll(); // 任何请求需要身份认证 registry.and() .authorizeRequests() .anyRequest() .authenticated() // 关闭跨站请求防护及不使用session .and() .csrf() .disable() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 自定义权限拒绝处理类 .and() .exceptionHandling() .accessDeniedHandler(restfulAccessDeniedHandler()) .authenticationEntryPoint(restAuthenticationEntryPoint()) // 自定义权限拦截器JWT过滤器 .and() .addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class); //有动态权限配置时添加动态权限校验过滤器 if(dynamicSecurityService!=null){ registry.and().addFilterBefore(dynamicSecurityFilter(), FilterSecurityInterceptor.class); } } /** * 用于配置UserDetailsService及PasswordEncoder; * @param auth * @throws Exception */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService()) .passwordEncoder(passwordEncoder()); } /** * :SpringSecurity定义的用于对密码进行编码及比对的接口,目前使用的是BCryptPasswordEncoder; * @return */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } /** * 在用户名和密码校验前添加的过滤器,如果有jwt的token,会自行根据token信息进行登录 * @return */ @Bean public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter() { return new JwtAuthenticationTokenFilter(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } /** * 当用户没有访问权限时的处理器,用于返回JSON格式的处理结果; * @return */ @Bean public RestfulAccessDeniedHandler restfulAccessDeniedHandler() { return new RestfulAccessDeniedHandler(); } /** * 当未登录或token失效时,返回JSON格式的结果; * @return */ @Bean public RestAuthenticationEntryPoint restAuthenticationEntryPoint() { return new RestAuthenticationEntryPoint(); } @Bean public IgnoreUrlsConfig ignoreUrlsConfig() { return new IgnoreUrlsConfig(); } @Bean public JwtTokenUtil jwtTokenUtil() { return new JwtTokenUtil(); } @ConditionalOnBean(name = "dynamicSecurityService") @Bean public DynamicAccessDecisionManager dynamicAccessDecisionManager() { return new DynamicAccessDecisionManager(); } @ConditionalOnBean(name = "dynamicSecurityService") @Bean public DynamicSecurityFilter dynamicSecurityFilter() { return new DynamicSecurityFilter(); } @ConditionalOnBean(name = "dynamicSecurityService") @Bean public DynamicSecurityMetadataSource dynamicSecurityMetadataSource() { return new DynamicSecurityMetadataSource(); }}
明天讲MyMes中SpringSecurity项目配置类中相关依赖的实现