Spring Token安全控制组件的实现

Posted Lazy Gene

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring Token安全控制组件的实现相关的知识,希望对你有一定的参考价值。

安全知识介绍

  •   认证(authentication),  是对用户身份的确认,比如系统登录,输入的用户名密码就是要告诉系统我是我;
  •   授权(ahthorization), 是对身份的权限控制,就像神盾局中特工一样,你虽然通过身份确认走进了神盾局大厦,但是级别不够,很多资料并没有访问权限;

 

  在单应用项目中,尤其是作为移动端APP后台服务项目,向客户端提供的接口往往需要考虑安全问题,最常用也最简单的实现方式是用户在手机端通过输入用户名和密码(或者手机号、邮箱和验证码)登录系统,登录成功后后台返回一个带有时效的token给客户端,客户端在访问其他资源时,请求中(一般在消息头中)携带这个token值。

  不难看出客户端每一次访问后端资源时都要告诉后台“我是我”,登录时通过用户名和密码(手机号、邮箱和验证码)的方式,而在之后的资源访问中,是以token值的方式来证明“我是我”。后端的实现的方式也不难,第一次访问时,只需要比对用户输入的用户名和密码与他注册时填写的用户名密码是否一致,如果一致则生成一个token,把它和和用户ID关联,保存到内存或者数据库(常用mongodb/redis)中,并将这个token返回给客户端;当客户端再次访问其他资源时,只需要比对请求中携带的token和上次保存内存或数据库中的token,当然除了检验是否一致外,还要看看有没有过期。至此我们就完成了一个简易的安全认证过程。

(竟然是第一次画时序图,好丑)

 

项目搭建

  既然是组件开发,当然是希望它能用在多个项目中,所有要有一定的灵活度,目前大部分项目都是基于spring (spring boot)的,所以直接创建一个spring boot 项目,在这个项目里抽象需要的接口。创建项目:http://start.spring.io/

接口抽象

为了能够让组件可以灵活的运用于多个项目,采用接口组合的方式进行设计;

用户接口

  认证的目的是证明“我是我”,这里的“我”就是用户,目前只考虑认证部分(授权后续逐渐增加),比如用户需要密码验证,那么就需要从数据库中获取用户名密码。

package com.iflytek.talon.security.core;

/**
 * 用户接口,具体应用中系统用户实现该接口。
 * @author Lazy Gene(ljgeng@iflytek.com)
 *
 */
public interface User {
    
    /**
     * 获取身份的标识,如果如用户的登录名,手机号,邮箱等,标识必须是唯一的。
     * @return 能够代表用户身份的字符串
     */
    String getIdentity();
    
    /**
     * 返回用户请求中的密钥部分,可以是密码、验证码或者证书内容等。
     * @return 密码、验证码等需要认证的信息
     */
    String getPasscode();
    
    /**
     * 用来判断用户是否被注销,大部分系统中用户都有注销或者删除的操作,对于已经注销的用户,
     * 必然不能通过认证。
     * @return 返回<code>True</code>表示用户可用,反之不可用 
     */
    boolean isEnabled();
}

 

token接口

  对于token, 希望知道token具体的值,对应的用户是谁,是否过了有效期,所以在接口中定义这些方法。

package com.iflytek.talon.security.core;

import java.time.LocalDateTime;
import java.util.Optional;

/**
 * token 接口,提供了token验证最少信息
 * @author Lazy Gene(ljgeng@iflytek.com)
 *
 */
public interface Token {
    
    /**
     * token 值,往往是一串加密的字符串,且该值应该是唯一的
     * @return 唯一的字符串,可以使用UUID生成
     */
    String getValue();
    
    /**
     * 获取token关联的用户
     * @return  含有 {@link User} 值的 {@code Optional}对象
     */
    Optional<User> getUser(); 
    
    /**
     * 获取token到期的时间,当这个时间已经小于系统时间,表明token过期
     * @return {@link LocalDateTime} token到期的时间
     */
    LocalDateTime getExpireTime();
    
    
    /**
     * 获取最近一次的登录刷新时间
     * <p>设置token有效期时,如三天,那么过期时间就是当前时间加三天,当用户第二天登录时,有效期
     * 应该顺延一天,所以需要记录用户的最新的登录时间 ,实际上也不需要用户每次请求都去刷新token,
     * 具体原来可参考{@link TokenOptions}</p>
     * @return {@link LocalDateTime} 记录上一次的登录时间
     */
    LocalDateTime getLastTime();
}

 

token管理接口

  对于token对象的管理,除了正常的增删改查意外,还应能够设置token的有效时长,token的支持模式(token数量是否限制,限制的话最多运行多少个)。对于后者主要是定义token的配置项,不妨定义一个配置接口

 

package com.iflytek.talon.security.core;

/**
 * Token 的配置项
 * <ul>
 *     <li>对于token的有效设置,支持场景为:当用户连续一周(可配)未登录,再使用系统需要重新登录</li>
 * <li>token数量的限定设置</li>
 * </ul>
 * @author Lazy Gene(ljgeng@iflytek.com)
 *
 */
public interface TokenOptions {
    
    /**
     * 获取token允许的空闲时长,比如一周
     * @return 秒数,如一周 60*60*24*7
     */
    int getTimeout();
    
    /**
     * token 的刷新间隔
     * <p>用户每次请求都去更新token的最后登录时间,会比较浪费资源,实际上只需要适当的设置刷新间隔,
     * 从而能够更高效的使用系统资源,比如超时时间为一周时,那么设置间隔时间为1小时(一周的1小时误差
     * 是可以接受的),但是大大的降低了刷新数据库的频率</p>
     * @return 秒数
     */
    int getInterval();
    
    /**
     * 是否允许大量的token
     * @return <code>True</code> 表示对token的数量不限制,反之限制
     */
    boolean isMassive();
    
    /**
     * 当massive为false时,该配置生效,表示最多允许token同时存在的个数
     * @return token 同一用户token最大在线个数
     */
    int getMax();
    
}

 

  进一步定义Token管理接口

package com.iflytek.talon.security.core;

import java.util.Optional;

/**
 * token 的管理接口,负责创建token, 设置token配置等
 * @author Lazy Gene(ljgeng@iflytek.com)
 *
 */
public interface TokenManager {
    
    /**
     * 获取token配置项
     * @return {@link TokenOptions} 配置项实例
     */
    TokenOptions getOptions();
    
    /**
     * 设置token配置
     * @param options {@link TokenOptions} 配置项实例
     */
    void setOptions(TokenOptions options);
    
    /**
     * 创建token,向数据库中保存token
     * @param token {@link Token}实现类的一个具体实例
     * @return token {@link Token}实现类的一个具体实例, 其value不能为<code>null</code>
     */
    Token save(Token token);
    
    /**
     * 更新Token, 因为Token接口设计比较简单,这个方法的主要作用就在于延长过期时间
     * @param token
     */
    void update(Token token);
    
    /**
     *  查找Token
     * @param value token 具体的值
     * @return 含有 {@link Token} 值的 {@code Optional}对象
     */
    Optional<Token> get(String value);
    
    /**
     * 删除指定的Token
     * @param value token 具体的值
     */
    void remove(String value);
    
    /**
     * 清空该用户的所有token
     * @param user {@link User} 用户实例
     */
    void clear(User user);
    
}

 

认证接口

 我们只是基于token的认证,那么对于认证来说,其实只需要检验token即可,所以该接口只定义一个方法

package com.iflytek.talon.security.core;

import java.util.Optional;

/**
 * 认证接口
 * @author Lazy Gene(ljgeng@iflytek.com)
 *
 */
public interface Authentication {
    
    /**
     * 获取用户认证的信息,也就是token
     * @return 含有 {@link Token} 的{@code Optional} 值
     */
    Optional<Token> getToken();
}

 

  对请求认证,所以要有用户的认证请求,正如上文提到了,认证的请求可以是多种,如未登录时可能使用用户名密码,手机号验证码等,登录后使用的token, 故而也要定义接口

package com.iflytek.talon.security.core;

/**
 * 认证请求
 * 
 * @author Lazy Gene(ljgeng@iflytek.com)
 *
 */
public interface AuthenticationRequest {
    
    /**
     * 对于用户名密码认证,这里就是用户名,对于token认证,这个就是token值
     * @return 代表认证身份的唯一标识,如用户名,token值等
     */
    Object getPrincipal();

    /**
     * 对于用户名密码认证,这里就是密码
     * @return 对认证身份的识别信息,如密码,验证码
     */
    Object getCredentials();
}

 

认证管理接口

  在用户登录时,成功则返回认证的信息,失败则返回空对象,另外用户登出时注销认证,在认证管理接口中定义登录登出接口。

package com.iflytek.talon.security.core;

import java.util.Optional;

/**
 * 认证管理
 * @author Lazy Gene(ljgeng@iflytek.com)
 *
 */
public interface AuthenticationManager {
    
    /**
     * 用户登录认证
     * @param request {@link AuthenticationRequest} 认证请求的具体实例
     * @return 含有 {@link Authentication} 值的 {@code Optional}对象
     */
    Optional<Authentication> login(AuthenticationRequest request);
    
    /**
     * 登出操作,注销登录信息
     * @param authentication 认证信息 {@code Authentication}
     */
    void logout(Authentication authentication);
}

安全上下文

  用户登录成功后,在当前的请求中,可能会有很多代码用到认证的信息,这时我们需要有一个上下文来保存我们的认证信息,方便在代码的各处调用,所以定义上下文接口

package com.iflytek.talon.security.core;

import java.util.Optional;

/**
 * 安全上下文,通过上下文可以获取到用户认证信息
 * @author Lazy Gene(ljgeng@iflytek.com)
 *
 */
public interface SecurityContext {
    
    /**
     * 获取认证信息
     * @return 存放{@link Authentication}的{@code Optional}对象
     */
    Optional<Authentication> getAuthentication();
    
    /**
     * 设置认证信息
     * @param authentication {@link Authentication}用户认证信息
     */
    void setAuthentication(Authentication authentication);
    
    /**
     * 安全上下文默认实现
     * @author Lazy Gene(ljgeng@iflytek.com)
     *
     */
    public static class SecurityContextImpl implements SecurityContext{
        
        private Authentication authentication;
        
        @Override
        public Optional<Authentication> getAuthentication() {
            return Optional.ofNullable(this.authentication);
        }

        @Override
        public void setAuthentication(Authentication authentication) {
            this.authentication = authentication;
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + ((authentication == null) ? 0 : authentication.hashCode());
            return result;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            SecurityContextImpl other = (SecurityContextImpl) obj;
            if (authentication == null) {
                if (other.authentication != null)
                    return false;
            } else if (!authentication.equals(other.authentication))
                return false;
            return true;
        }

        @Override
        public String toString() {
            return "SecurityContextImpl [authentication=" + authentication + "]";
        }
        
    }
}

 

最后,定义一个下文管理类来获取上下文,将上下文对象存储在TheadLocal中,保证各线程上下文的唯一性

package com.iflytek.talon.security.core;

import org.springframework.util.Assert;

import com.iflytek.talon.security.core.SecurityContext.SecurityContextImpl;

/**
 * 安全上下文管理
 * @author Lazy Gene(ljgeng@iflytek.com)
 *
 */
public class SecurityContextManager {
    
    /**
     * 利用ThreadLocal 存储安全上下文
     */
    private final static ThreadLocal<SecurityContext> CONTEXT = new ThreadLocal<SecurityContext>();
    
    /**
     * 获取上下文,从ThreadLocal中获取,获取不到时,创建一个不含认证信息的上下文,将其存放到ThreadLocal,
     * 并将该上下文返回
     * @return {@link SecurityContext} 安全上下文,不会为null
     */
    public static SecurityContext getContext() {
        SecurityContext sctx = CONTEXT.get();
        if (sctx == null) {
            sctx = new SecurityContextImpl();
            CONTEXT.set(sctx);
        }
        return sctx;
    }
    
    /**
     * 设置安全上下文,如果上下文为<code>NULL</code>,则会直接抛出非法参数异常{@link IllegalArgumentException} 
     * @param context
     */
    public static void set(SecurityContext context) {
        Assert.notNull(context,"安全上下文不能为空");
        CONTEXT.set(context);
    }
    
    /**
     * 调用ThreadLocal remove 方法,防止内存泄露
     */
    public static void clear() {
        CONTEXT.remove();
    }
}

至此我们完成了认证需要的一些核心接口,下文将具体来实现这个接口。

 

注:本文设计参考了 开源项目 https://github.com/melthaw/spring-security-token

以上是关于Spring Token安全控制组件的实现的主要内容,如果未能解决你的问题,请参考以下文章

spring boot2整合shiro安全框架实现前后端分离的JWT token登录验证

Spring cloud微服务安全实战-5-9实现基于session的SSO(Token有效期)

Laravel:如何在控制器的几种方法中重用代码片段

Spring Security实现OAuth2.0——资源服务

Spring boot + JWT 实现安全验证 ---auth0.jwt

spring练习,在Eclipse搭建的Spring开发环境中,使用set注入方式,实现对象的依赖关系,通过ClassPathXmlApplicationContext实体类获取Bean对象(代码片段