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([email protected])
 *
 */
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([email protected])
 *
 */
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([email protected])
 *
 */
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([email protected])
 *
 */
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([email protected])
 *
 */
public interface Authentication {

    /**
     * 获取用户认证的信息,也就是token
     * @return 含有 {@link Token} 的{@code Optional} 值
     */
    Optional<Token> getToken();
}

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

package com.iflytek.talon.security.core;

/**
 * 认证请求
 *
 * @author Lazy Gene([email protected])
 *
 */
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([email protected])
 *
 */
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([email protected])
 *
 */
public interface SecurityContext {

    /**
     * 获取认证信息
     * @return 存放{@link Authentication}的{@code Optional}对象
     */
    Optional<Authentication> getAuthentication();

    /**
     * 设置认证信息
     * @param authentication {@link Authentication}用户认证信息
     */
    void setAuthentication(Authentication authentication);

    /**
     * 安全上下文默认实现
     * @author Lazy Gene([email protected])
     *
     */
    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([email protected])
 *
 */
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

原文地址:https://www.cnblogs.com/ljgeng/p/9129464.html

时间: 2024-10-17 11:31:21

Spring Token安全控制组件的实现(一)的相关文章

对spring控制反转以及依赖注入的理解

一.说到依赖注入(控制反转),先要理解什么是依赖. Spring 把相互协作的关系称为依赖关系.假如 A 组件调用了 B 组件的方法,我们可称A 组件依赖于 B 组件. 二.什么是依赖注入. 在传统的程序设计过程中,通常由调用者来创建被调用者的实例. 在依赖注入的模式下,创建被调用者的工作不再由调用者来完成,因此称为控制反转:创建被调用者实例的工作通常由Spring 容器来完成,然后注入给调用者,因此也称为依赖注入. 自己理解:即一句话,由spring容器来控制组件A的调用的具体对象B.组件A依

跟我学SpringCloud | 第十一篇:使用Spring Cloud Sleuth和Zipkin进行分布式链路跟踪

SpringCloud系列教程 | 第十一篇:使用Spring Cloud Sleuth和Zipkin进行分布式链路跟踪 Springboot: 2.1.6.RELEASE SpringCloud: Greenwich.SR1 如无特殊说明,本系列教程全采用以上版本 在分布式服务架构中,需要对分布式服务进行治理--在分布式服务协同向用户提供服务时,每个请求都被哪些服务处理?在遇到问题时,在调用哪个服务上发生了问题?在分析性能时,调用各个服务都花了多长时间?哪些调用可以并行执行?-- 为此,分布式

【微信】微信获取TOKEN,以及储存TOKEN方法,Spring quartz让Token永不过期

官网说明 access_token是公众号的全局唯一票据,公众号调用各接口时都需使用access_token.开发者需要进行妥善保存.access_token的存储至少要保留512个字符空间.access_token的有效期目前为2个小时,需定时刷新,重复获取将导致上次获取的access_token失效. 公众平台的API调用所需的access_token的使用及生成方式说明: 1.为了保密appsecrect,第三方需要一个access_token获取和刷新的中控服务器.而其他业务逻辑服务器所

What is the best way to handle Invalid CSRF token found in the request when session times out in Spring security

18.5.1 Timeouts One issue is that the expected CSRF token is stored in the HttpSession, so as soon as the HttpSession expires your configured AccessDeniedHandler will receive a InvalidCsrfTokenException. If you are using the default AccessDeniedHandl

Spring Security框架下Restful Token的验证方案

项目使用Restful的规范,权限内容的访问,考虑使用Token验证的权限解决方案. 验证方案(简要概括): 首先,用户需要登陆,成功登陆后返回一个Token串: 然后用户访问有权限的内容时需要上传Token串进行权限验证 代码方案: Spring MVC + Spring Security + Redis的框架下实现权限验证,此文重点谈谈Spring Security下的Token验证实现. 首先,看看spring security的配置: <http pattern="/service

spring cloud云架构 - SSO单点登录之OAuth2.0 根据token获取用户信息(4)

上一篇我根据框架中OAuth2.0的使用总结,画了SSO单点登录之OAuth2.0 登出流程,今天我们看一下根据用户token获取yoghurt信息的流程: Java代码   /** * 根据token获取用户信息 * @param accessToken * @return * @throws Exception */ @RequestMapping(value = "/user/token/{accesstoken}", method = RequestMethod.GET) pu

redis jwt spring boot spring security 实现api token 验证

文章地址:http://www.haha174.top/article/details/258083 项目源码:https://github.com/haha174/jwt-token.git 具体的实际效果可以看考这里 目前已经部署一个 个人测试机器上面: http://cloud.codeguoj.cn/api-cloud-server/swagger-ui.html#!/token45controller/loginUsingPOST 相信很多人都调用过api, 一般的大致基本步骤都是先用

Spring Cloud云架构 SSO单点登录之OAuth2.0 根据token获取用户信息(4)

上一篇我根据框架中OAuth2.0的使用总结,画了SSO单点登录之OAuth2.0 登出流程,今天我们看一下根据用户token获取yoghurt信息的流程: /** * 根据token获取用户信息 * @param accessToken * @return * @throws Exception */ @RequestMapping(value = "/user/token/{accesstoken}", method = RequestMethod.GET) public Resp

spring cloud 服务A调用服务B自定义token消失,记录

后端:spring cloud 前端:vue 场景:前端ajax请求,包装自定义请求头token到后台做验证,首先调用A服务,A服务通过Feign调用B服务发现自定义token没有传到B服务去; 原因:cloud 服务之间的调用都是基于Feign的,所以我们可以在调用之前做一些事情,在请求头header中添加自定义请求头token 首先定义一个feign的拦截器,达到在发送请求前认证token的目的' 定义一个配置类 @Configuration // 说明该类是配置类 public class