JWT设计原理 JWT结合spring-security在项目中的应用
- JWT
JWT译文
- 什么是JWT
1. 开放标准
2. 数字签名 支持HMAC,RSA,ECDSA加密
3. 验签可以保证token的完整性即当token内容被篡改的时候可以通过验签发现
4. 当使用加密后可以保证token内容不外泄,仅持有私钥的一方才能将token解开
- 什么时候用JWT
1. 鉴权 支持单点登录 开销小 方便跨域
2. 信息交换 JWT支持加密签名 所以可以安全的传递信息 可做验签和解密验证发送方是否可靠
- JWT的标准结构应该是什么样的
1. JWT分为三段 头信息 负载信息 签名
2. 头信息 通常由签名算法+令牌类型组成
3. 中部有效负载
1. 推荐添加到期时间和主题等信息
2. 可以任意添加信息 但是注意如果非加密方式的token 建议token内不要包含敏感信息 因为token是暴露在外的
4. 签名 需要将头信息和负载内容一起做签名 验签的时候可以避免信息被篡改
SPRING-SECURITY译文
spring-security
-
特性
- 支持身份验证,授权,防范常见攻击
- 支持集成
-
基础组件
- SecurityContextHolder 存储和获取验证后信息
SecurityContextHolder.getContext().getAuthentication();
- SecurityContext 从SecurityContextHolder中获得的上下文信息 包含认证信息
- Authentication 不同阶段的鉴权对象 如:鉴权后的当前登陆人或鉴权前的PreAuthenticatedAuthenticationToken(预处理拦截器先处理得到预处理token再调用AuthenticationManager得到最终token)
- GrantedAuthority 授予鉴权对象的权限 如:角色 范围等
- AuthenticationManager 具体Filter如何执行身份验证的API
- ProviderManager 是AuthenticationManager的具体实现
- 首先实现AuthenticationProvider,注意里面的support方法 决定了Provider到底处理那种类型的Authentication,如上第二点所说Authentication 存在多种类型
public interface AuthenticationProvider { // ~ Methods // ======================================================================================================== /** * Performs authentication with the same contract as * {@link org.springframework.security.authentication.AuthenticationManager#authenticate(Authentication)} * . * * @param authentication the authentication request object. * * @return a fully authenticated object including credentials. May return *
null
if theAuthenticationProvider
is unable to support * authentication of the passedAuthentication
object. In such a case, * the nextAuthenticationProvider
that supports the presented *Authentication
class will be tried. * * @throws AuthenticationException if authentication fails. */ Authentication authenticate(Authentication authentication) throws AuthenticationException; /** * Returnstrue
if thisAuthenticationProvider
supports the * indicatedAuthentication
object. ** Returning
*true
does not guarantee an *AuthenticationProvider
will be able to authenticate the presented * instance of theAuthentication
class. It simply indicates it can * support closer evaluation of it. AnAuthenticationProvider
can still * returnnull
from the {@link #authenticate(Authentication)} method to * indicate anotherAuthenticationProvider
should be tried. ** Selection of an
* * @param authentication * * @returnAuthenticationProvider
capable of performing * authentication is conducted at runtime theProviderManager
. *true
if the implementation can more closely evaluate the *Authentication
class presented */ boolean supports(Class> authentication); }- 其次ProviderManager的authenticate会遍历所有Provider(getProviders),然后找到上面提到的支持当前Authentication类型的Provider做处理
public Authentication authenticate(Authentication authentication) throws AuthenticationException { Class extends Authentication> toTest = authentication.getClass(); AuthenticationException lastException = null; AuthenticationException parentException = null; Authentication result = null; Authentication parentResult = null; boolean debug = logger.isDebugEnabled(); for (AuthenticationProvider provider : getProviders()) { if (!provider.supports(toTest)) { continue; } if (debug) { logger.debug("Authentication attempt using " + provider.getClass().getName()); }
AuthenticationProvider ProviderManager众多执行者中的一个,如上面所讲,满足类型的AuthenticationProvider将被执行
AuthenticationEntryPoint 对于鉴权过程中如异常等响应的统一处理
-
AbstractAuthenticationProcessingFilter
- 以UsernamePasswordAuthenticationFilter为例,主要是实现attemptAuthentication方法将request中的参数进行封装,变为Authentication,再传递给下游AuthenticationManager
-
DaoAuthenticationProvider
- DaoAuthenticationProvider会从UserDetailsService中加载用户信息,然后与传递过来的用户名密码进行比较
//如何定义DaoAuthenticationProvider及注入UserDetailsService //继承WebSecurityConfigurerAdapter并重写configure方法 @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //第一次登陆账号密码验证Provider //默认使用BCryptPasswordEncoder比对加密后的密码 daoProvider.setPasswordEncoder(); //验证方法为spring-security内部提供的DaoAuthenticationProvider.additionalAuthenticationChecks DaoAuthenticationProvider daoProvider = new DaoAuthenticationProvider(); daoProvider.setUserDetailsService(jwtUserDetailsService); daoProvider.setPasswordEncoder(new Md5PasswordEncoder()); //定义两个Provider daoProvider负责UserNameAndPasswordToken登录验证 auth.authenticationProvider(daoProvider); }
UserDetailsService 获取当前登录用户信息,实现UserDetailsService然后返回UserDetails
public interface UserDetailsService { // ~ Methods // ======================================================================================================== /** * Locates the user based on the username. In the actual implementation, the search * may possibly be case sensitive, or case insensitive depending on how the * implementation instance is configured. In this case, the
UserDetails
* object that comes back may have a username that is of a different case than what * was actually requested.. * * @param username the username identifying the user whose data is required. * * @return a fully populated user record (nevernull
) * * @throws UsernameNotFoundException if the user could not be found or the user has no * GrantedAuthority */ UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; }- FilterInvocationSecurityMetadataSource 为当前请求的URL打上一些标签,如:当前的URL需要什么资源可以访问,ConfigAttribute为接口可以自己定义实现
@Override public Collection
getAttributes(Object o) { //FilterInvocation filterInvocation=Object o; 获取当前request //当前URL的特殊标签 //获取什么资源可以允许当前request然后将资源id封装后返回 } @Override public Collection getAllConfigAttributes() { //全局标签 return Collections.emptyList(); } @Override public boolean supports(Class> aClass) { //什么类型的请求可以走此封装 return FilterInvocation.class.isAssignableFrom(aClass); } - AccessDecisionManager 授权决策接口 跟FilterInvocationSecurityMetadataSource配套使用
//根据之前提到的AuthenticationManager封装的Authentication中的角色信息及FilterInvocationSecurityMetadataSource中的请求标签 判断当前的角色是否有操作resourceIds的权限 public void decide(Authentication auth, Object o, Collection
resourceIds) //开启自定义资源认证 //@EnableWebSecurity //public class WebSecurityConfig extends WebSecurityConfigurerAdapter @Override protected void configure(HttpSecurity http) throws Exception { //customMetadataSourceService每次请求根据数据库配置读取资源元信息及所需权限 并通过urlAccessDecisionManager与当前登录人所包含的权限进行比对 http.authorizeRequests().withObjectPostProcessor(new ObjectPostProcessor
() { @Override public O postProcess(O o) { o.setSecurityMetadataSource(customMetadataSourceService); o.setAccessDecisionManager(urlAccessDecisionManager); return o; } }).anyRequest().permitAll() - AuthenticationSuccessHandler 请求成功后处理 这个就不详细介绍了
-
认证机制
- 因为我们主要说JWT所以简单说一下 Pre-Authentication Scenarios 当已经做了外部鉴权,到spring-security直接可用,即预验证场景
- 首先需要实现AbstractPreAuthenticatedProcessingFilter,这里主要是实现方法getPreAuthenticatedPrincipal,从request中获取预授权信息
- setCheckForPrincipalChanges(true),用来保证security上下文发生变更时候会走此预授权
- AbstractPreAuthenticatedProcessingFilter内部会将principal封装成PreAuthenticatedAuthenticationToken(Authentication)并传递给下游AuthenticationManager
- AuthenticationManager完成验证并返回实际Authentication将会存在SecurityContextHolder中便于在系统中获取当前人员
- 因为我们主要说JWT所以简单说一下 Pre-Authentication Scenarios 当已经做了外部鉴权,到spring-security直接可用,即预验证场景
-
上图 图1是普通登录生成token的过程 图2为使用token进行鉴权的过程
-
对于JWT实现方式的一些探讨 能否借助redis做密钥生成 满足自动过期和仅允许一人登录 答案是可以的 下面就分几步简单介绍一下
- header和payload不做探讨了 就是标准结构 两个JSON 且不包含敏感信息
- 首先根据用户名+UUID(或任意比较复杂的随机方案) 生成一个当前用户的secret 并将secret保存在redis 如JWT_AAA_SEC=***
- 然后将header和payload+secret通过hmacSha256Base64做一个签名为sign token为base64 header . base64 payload . sign
- 当有请求时 首先根据username从redis中获取secret
- 然后重复3中步骤生成sign并与当前token的sign做比较 如果不一致验签失败
- 那重复登录踢出和自动过期的实现方式就很显然了 不详细说了