spring学习之springSecurity(七)之微服务鉴权

我爱海鲸 2022-11-21 20:00:15 java、jwt

简介微服务权限方案、jwt介绍

项目地址:https://gitee.com/liu-haijin/acl_parent

1、 什么是微服务

微服务由来

微服务最早由 Martin Fowler 与 James Lewis 于 2014 年共同提出,微服务架构风格是一种
使用一套小服务来开发单个应用的方式途径,每个服务运行在自己的进程中,并使用轻量
级机制通信,通常是 HTTP API,这些服务基于业务能力构建,并能够通过自动化部署机制
来独立部署,这些服务使用不同的编程语言实现,以及不同数据存储技术,并保持最低限
度的集中式管理。 

微服务优势

(1)微服务每个模块就相当于一个单独的项目,代码量明显减少,遇到问题也相对来说比
较好解决。
(2)微服务每个模块都可以使用不同的存储方式(比如有的用 redis,有的用 mysql
等),数据库也是单个模块对应自己的数据库。
(3)微服务每个模块都可以使用不同的开发技术,开发模式更灵活。

微服务本质 

(1)微服务,关键其实不仅仅是微服务本身,而是系统要提供一套基础的架构,这种架构
使得微服务可以独立的部署、运行、升级,不仅如此,这个系统架构还让微服务与微服务
之间在结构上“松耦合”,而在功能上则表现为一个统一的整体。这种所谓的“统一的整
体”表现出来的是统一风格的界面,统一的权限管理,统一的安全策略,统一的上线过
程,统一的日志和审计方法,统一的调度方式,统一的访问入口等等。
(2)微服务的目的是有效的拆分应用,实现敏捷开发和部署。

2、微服务认证与授权实现思路

认证授权过程分析

(1)如果是基于 Session,那么 Spring-security 会对 cookie 里的 sessionid 进行解析,找
到服务器存储的 session 信息,然后判断当前用户是否符合请求的要求。
(2)如果是 token,则是解析出 token,然后将当前请求加入到 Spring-security 管理的权限
信息中去

如果系统的模块众多,每个模块都需要进行授权与认证,所以我们选择基于 token 的形式
进行授权与认证,用户根据用户名密码认证成功,然后获取当前用户角色的一系列权限
值,并以用户名为 key,权限列表为 value 的形式存入 redis 缓存中,根据用户名相关信息
生成 token 返回,浏览器将 token 记录到 cookie 中,每次调用 api 接口都默认将 token 携带
到 header 请求头中,Spring-security 解析 header 头获取 token 信息,解析 token 获取当前
用户名,根据用户名就可以从 redis 中获取权限列表,这样 Spring-security 就能够判断当前
请求是否有权限访问 

权限管理数据模型

 jwt 介绍

访问令牌的类型

JWT 的组成 

典型的,一个 JWT 看起来如下图:

该对象为一个很长的字符串,字符之间通过"."分隔符分为三个子串。
每一个子串表示了一个功能块,总共有以下三个部分:JWT 头、有效载荷和签名

JWT 头 

JWT 头部分是一个描述 JWT 元数据的 JSON 对象,通常如下所示。

{
"alg": "HS256",
"typ": "JWT"
}

在上面的代码中,alg 属性表示签名使用的算法,默认为 HMAC SHA256(写为 HS256);
typ 属性表示令牌的类型,JWT 令牌统一写为 JWT。最后,使用 Base64 URL 算法将上述
JSON 对象转换为字符串保存。 

有效载荷

有效载荷部分,是 JWT 的主体内容部分,也是一个 JSON 对象,包含需要传递的数据。 JWT
指定七个默认字段供选择。

iss:发行人
exp:到期时间
sub:主题
aud:用户
nbf:在此之前不可用
iat:发布时间
jti:JWT ID 用于标识该 JWT
除以上默认字段外,我们还可以自定义私有字段,如下例:

{
"sub": "1234567890",
"name": "Helen",
"admin": true
}

请注意,默认情况下 JWT 是未加密的,任何人都可以解读其内容,因此不要构建隐私信息
字段,存放保密信息,以防止信息泄露。
JSON 对象也使用 Base64 URL 算法转换为字符串保存。

签名哈希

签名哈希部分是对上面两部分数据签名,通过指定的算法生成哈希,以确保数据不会被篡
改。
首先,需要指定一个密码(secret)。该密码仅仅为保存在服务器中,并且不能向用户公
开。然后,使用标头中指定的签名算法(默认情况下为 HMAC SHA256)根据以下公式生成
签名。
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(claims), secret)
在计算出签名哈希后,JWT 头,有效载荷和签名哈希的三个部分组合成一个字符串,每个
部分用"."分隔,就构成整个 JWT 对象。 

Base64URL 算法

如前所述,JWT 头和有效载荷序列化的算法都用到了 Base64URL。该算法和常见 Base64 算
法类似,稍有差别。
作为令牌的 JWT 可以放在 URL 中(例如 api.example/?token=xxx)。 Base64 中用的三个
字符是"+","/"和"=",由于在 URL 中有特殊含义,因此 Base64URL 中对他们做了替换:
"="去掉,"+"用"-"替换,"/"用"_"替换,这就是 Base64URL 算法。 

具体代码实现

项目截图:

核心配置

编写核心配置类

Spring Security 的核心配置就是继承 WebSecurityConfigurerAdapter 并注解
@EnableWebSecurity 的配置。这个配置指明了用户名密码的处理方式、请求路径、登录
登出控制等和安全相关的配置

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class TokenWebSecurityConfig extends WebSecurityConfigurerAdapter {

    private TokenManager tokenManager;
    private RedisTemplate redisTemplate;
    private DefaultPasswordEncoder defaultPasswordEncoder;
    private UserDetailsService userDetailsService;

    @Autowired
    public TokenWebSecurityConfig(UserDetailsService userDetailsService, DefaultPasswordEncoder defaultPasswordEncoder,
                                  TokenManager tokenManager, RedisTemplate redisTemplate) {
        this.userDetailsService = userDetailsService;
        this.defaultPasswordEncoder = defaultPasswordEncoder;
        this.tokenManager = tokenManager;
        this.redisTemplate = redisTemplate;
    }

    /**
     * 配置设置
     * @param http
     * @throws Exception
     */
    //设置退出的地址和token,redis操作地址
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.exceptionHandling()
                .authenticationEntryPoint(new UnauthEntryPoint())//没有权限访问
                .and().csrf().disable()
                .authorizeRequests()
                .anyRequest().authenticated()
                .and().logout().logoutUrl("/admin/acl/index/logout")//退出路径
                .addLogoutHandler(new TokenLogoutHandler(tokenManager,redisTemplate)).and()
                .addFilter(new TokenLoginFilter(authenticationManager(), tokenManager, redisTemplate))
                .addFilter(new TokenAuthFilter(authenticationManager(), tokenManager, redisTemplate)).httpBasic();
    }

    //调用userDetailsService和密码处理
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(defaultPasswordEncoder);
    }
    //不进行认证的路径,可以直接访问
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/api/**");
    }
}

DefaultPasswordEncoder:密码处理的方法

@Component
public class DefaultPasswordEncoder implements PasswordEncoder {

    public DefaultPasswordEncoder() {
        this(-1);
    }
    public DefaultPasswordEncoder(int strength) {
    }
    //进行MD5加密
    @Override
    public String encode(CharSequence charSequence) {
        return MD5.encrypt(charSequence.toString());
    }
    //进行密码比对
    @Override
    public boolean matches(CharSequence charSequence, String encodedPassword) {
        return encodedPassword.equals(MD5.encrypt(charSequence.toString()));
    }
}

TokenManager:token 操作的工具类

@Component
public class TokenManager {
    //token有效时长
    private long tokenEcpiration = 24*60*60*1000;
    //编码秘钥
    private String tokenSignKey = "123456";
    //1 使用jwt根据用户名生成token
    public String createToken(String username) {
        String token = Jwts.builder().setSubject(username)
                .setExpiration(new Date(System.currentTimeMillis()+tokenEcpiration))
                .signWith(SignatureAlgorithm.HS512, tokenSignKey).compressWith(CompressionCodecs.GZIP).compact();
        return token;
    }
    //2 根据token字符串得到用户信息
    public String getUserInfoFromToken(String token) {
        String userinfo = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token).getBody().getSubject();
        return userinfo;
    }
    //3 删除token
    public void removeToken(String token) { }
}

TokenLogoutHandler:退出实现 

//退出处理器
public class TokenLogoutHandler implements LogoutHandler {
    private TokenManager tokenManager;
    private RedisTemplate redisTemplate;

    public TokenLogoutHandler(TokenManager tokenManager,RedisTemplate redisTemplate) {
        this.tokenManager = tokenManager;
        this.redisTemplate = redisTemplate;
    }
    @Override
    public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
        //1 从header里面获取token
        //2 token不为空,移除token,从redis删除token
        String token = request.getHeader("token");
        if(token != null) {
            //移除
            tokenManager.removeToken(token);
            //从token获取用户名
            String username = tokenManager.getUserInfoFromToken(token);
            redisTemplate.delete(username);
        }
        ResponseUtil.out(response, R.ok());
    }
}

UnauthorizedEntryPoint:未授权统一处理

public class UnauthEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        ResponseUtil.out(httpServletResponse, R.error());
    }
}

SecutityUser

@Data
public class SecurityUser implements UserDetails {

    //当前登录用户
    private transient User currentUserInfo;

    //当前权限
    private List<String> permissionValueList;

    public SecurityUser() {
    }

    public SecurityUser(User user) {
        if (user != null) {
            this.currentUserInfo = user;
        }
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        for(String permissionValue : permissionValueList) {
            if(StringUtils.isEmpty(permissionValue)) {
                continue;
            }
            SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permissionValue);
            authorities.add(authority);
        }

        return authorities;
    }

    @Override
    public String getPassword() {
        return currentUserInfo.getPassword();
    }

    @Override
    public String getUsername() {
        return currentUserInfo.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

User

@Data
@ApiModel(description = "用户实体类")
public class User implements Serializable {

    private static final long serialVersionUID = 1L;

    @ApiModelProperty(value = "微信openid")
    private String username;

    @ApiModelProperty(value = "密码")
    private String password;

    @ApiModelProperty(value = "昵称")
    private String nickName;

    @ApiModelProperty(value = "用户头像")
    private String salt;

    @ApiModelProperty(value = "用户签名")
    private String token;

}

TokenLoginFilter:认证的 filter

public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {

    private TokenManager tokenManager;
    private RedisTemplate redisTemplate;
    private AuthenticationManager authenticationManager;

    public TokenLoginFilter(AuthenticationManager authenticationManager, TokenManager tokenManager, RedisTemplate redisTemplate) {
        this.authenticationManager = authenticationManager;
        this.tokenManager = tokenManager;
        this.redisTemplate = redisTemplate;
        this.setPostOnly(false);
        this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/acl/login","POST"));
    }

    //1 获取表单提交用户名和密码
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException {
        //获取表单提交数据
        try {
         // multipart/form-data或application/json时,无法通过request.getParameter()获取到请求内容,此时只能通过request.getInputStream()和request.getReader() 
         // 方法获取请求内容
            User user = new ObjectMapper().readValue(request.getInputStream(), User.class);
            return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(),user.getPassword(),
                    new ArrayList<>()));
        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException();
        }
    }

    //2 认证成功调用的方法
    @Override
    protected void successfulAuthentication(HttpServletRequest request, 
                                            HttpServletResponse response, FilterChain chain, Authentication authResult)
            throws IOException, ServletException {
        //认证成功,得到认证成功之后用户信息
        SecurityUser user = (SecurityUser)authResult.getPrincipal();
        //根据用户名生成token
        String token = tokenManager.createToken(user.getCurrentUserInfo().getUsername());
        //把用户名称和用户权限列表放到redis
        redisTemplate.opsForValue().set(user.getCurrentUserInfo().getUsername(),user.getPermissionValueList());
        //返回token
        ResponseUtil.out(response, R.ok().data("token",token));
    }

    //3 认证失败调用的方法
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed)
            throws IOException, ServletException {
        ResponseUtil.out(response, R.error());
    }
}

TokenAuthFilter:授权 filter 

public class TokenAuthFilter extends BasicAuthenticationFilter {

    private TokenManager tokenManager;
    private RedisTemplate redisTemplate;
    public TokenAuthFilter(AuthenticationManager authenticationManager,TokenManager tokenManager,RedisTemplate redisTemplate) {
        super(authenticationManager);
        this.tokenManager = tokenManager;
        this.redisTemplate = redisTemplate;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        //获取当前认证成功用户权限信息
        UsernamePasswordAuthenticationToken authRequest = getAuthentication(request);
        //判断如果有权限信息,放到权限上下文中
        if(authRequest != null) {
            SecurityContextHolder.getContext().setAuthentication(authRequest);
        }
        chain.doFilter(request,response);
    }

    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
        //从header获取token
        String token = request.getHeader("token");
        if(token != null) {
            //从token获取用户名
            String username = tokenManager.getUserInfoFromToken(token);
            //从redis获取对应权限列表
            List<String> permissionValueList = (List<String>)redisTemplate.opsForValue().get(username);
            Collection<GrantedAuthority> authority = new ArrayList<>();
            for(String permissionValue : permissionValueList) {
                SimpleGrantedAuthority auth = new SimpleGrantedAuthority(permissionValue);
                authority.add(auth);
            }
            return new UsernamePasswordAuthenticationToken(username,token,authority);
        }
        return null;
    }

}

SpringSecurity 原理总结 

SpringSecurity 的过滤器介绍

SpringSecurity 采用的是责任链的设计模式,它有一条很长的过滤器链。现在对这条过滤
器链的 15 个过滤器进行说明:

(1) WebAsyncManagerIntegrationFilter:将 Security 上下文与 Spring Web 中用于
处理异步请求映射的 WebAsyncManager 进行集成。 

(2) SecurityContextPersistenceFilter:在每次请求处理之前将该请求相关的安全上
下文信息加载到 SecurityContextHolder 中,然后在该次请求处理完成之后,将
SecurityContextHolder 中关于这次请求的信息存储到一个“仓储”中,然后将
SecurityContextHolder 中的信息清除,例如在 Session 中维护一个用户的安全信
息就是这个过滤器处理的。 

(3) HeaderWriterFilter:用于将头信息加入响应中。

(4) CsrfFilter:用于处理跨站请求伪造。 

(5)LogoutFilter:用于处理退出登录。 

(6)UsernamePasswordAuthenticationFilter:用于处理基于表单的登录请求,从表单中
获取用户名和密码。默认情况下处理来自 /login 的请求。从表单中获取用户名和密码
时,默认使用的表单 name 值为 username 和 password,这两个值可以通过设置这个
过滤器的 usernameParameter 和 passwordParameter 两个参数的值进行修改。

(7)DefaultLoginPageGeneratingFilter:如果没有配置登录页面,那系统初始化时就会
配置这个过滤器,并且用于在需要进行登录时生成一个登录表单页面。

(8)BasicAuthenticationFilter:检测和处理 http basic 认证。 

(9)RequestCacheAwareFilter:用来处理请求的缓存。

(10)SecurityContextHolderAwareRequestFilter:主要是包装请求对象 request。 

(11)AnonymousAuthenticationFilter:检测 SecurityContextHolder 中是否存在
Authentication 对象,如果不存在为其提供一个匿名 Authentication。 

(12)SessionManagementFilter:管理 session 的过滤器

(13)ExceptionTranslationFilter:处理 AccessDeniedException 和
AuthenticationException 异常。

(14)FilterSecurityInterceptor:可以看做过滤器链的出口。

(15)RememberMeAuthenticationFilter:当用户没有登录而直接访问资源时, 从 cookie 
里找出用户的信息, 如果 Spring Security 能够识别出用户提供的 remember me cookie, 
用户将不必填写用户名和密码, 而是直接登录进入系统,该过滤器默认不开启。 

SpringSecurity 基本流程 

Spring Security 采取过滤链实现认证与授权,只有当前过滤器通过,才能进入下一个
过滤器:

绿色部分是认证过滤器,需要我们自己配置,可以配置多个认证过滤器。认证过滤器可以
使用 Spring Security 提供的认证过滤器,也可以自定义过滤器(例如:短信验证)。认
证过滤器要在 configure(HttpSecurity http)方法中配置,没有配置不生效。下面会重
点介绍以下三个过滤器:

UsernamePasswordAuthenticationFilter 过滤器:该过滤器会拦截前端提交的 POST 方式
的登录表单请求,并进行身份认证。

ExceptionTranslationFilter 过滤器:该过滤器不需要我们配置,对于前端提交的请求会
直接放行,捕获后续抛出的异常并进行处理(例如:权限访问限制)。

FilterSecurityInterceptor 过滤器:该过滤器是过滤器链的最后一个过滤器,根据资源
权限配置来判断当前请求是否有权限访问对应的资源。如果访问受限会抛出相关异常,并
由 ExceptionTranslationFilter 过滤器进行捕获和处理。

SpringSecurity 认证流程

认证流程是在 UsernamePasswordAuthenticationFilter 过滤器中处理的,具体流程如下
所示: 

 UsernamePasswordAuthenticationFilter 源码

当前端提交的是一个 POST 方式的登录表单请求,就会被该过滤器拦截,并进行身份认
证。该过滤器的 doFilter() 方法实现在其抽象父类 

AbstractAuthenticationProcessingFilter 中,查看相关源码:

*** 上述的 第二 过程调用了 UsernamePasswordAuthenticationFilter 的
attemptAuthentication() 方法,源码如下:

****上述的(3)过程创建的 UsernamePasswordAuthenticationToken 是
Authentication 接口的实现类,该类有两个构造器,一个用于封装前端请求传入的未认
证的用户信息,一个用于封装认证成功后的用户信息:

*** Authentication 接口的实现类用于存储用户认证信息,查看该接口具体定义:

ProviderManager 源码 

上述过程中,UsernamePasswordAuthenticationFilter 过滤器的
attemptAuthentication() 方法的(5)过程将未认证的 Authentication 对象传入
ProviderManager 类的 authenticate() 方法进行身份认证。

ProviderManager 是 AuthenticationManager 接口的实现类,该接口是认证相关的核心接 口,也是认证的入口。在实际开发中,我们可能有多种不同的认证方式,例如:用户名+ 密码、邮箱+密码、手机号+验证码等,而这些认证方式的入口始终只有一个,那就是 AuthenticationManager。在该接口的常用实现类 ProviderManager 内部会维护一个 List列表,存放多种认证方式,实际上这是委托者模式 (Delegate)的应用。每种认证方式对应着一个 AuthenticationProvider, AuthenticationManager 根据认证方式的不同(根据传入的 Authentication 类型判断)委托 对应的 AuthenticationProvider 进行用户认证。 

上述认证成功之后的(6)过程,调用 CredentialsContainer 接口定义的
eraseCredentials() 方法去除敏感信息。查看
UsernamePasswordAuthenticationToken 实现的 eraseCredentials() 方法,该方
法实现在其父类中:

认证成功/失败处理

上述过程就是认证流程的最核心部分,接下来重新回到
UsernamePasswordAuthenticationFilter 过滤器的 doFilter() 方法,查看认证成
功/失败的处理:

查看 successfulAuthentication()和unsuccessfulAuthentication()方法源码︰

SpringSecurity 权限访问流程

上一个部分通过源码的方式介绍了认证流程,下面介绍权限访问流程,主要是对
ExceptionTranslationFilter 过滤器和 FilterSecurityInterceptor 过滤器进行介绍。

ExceptionTranslationFilter 过滤器

该过滤器是用于处理异常的,不需要我们配置,对于前端提交的请求会直接放行,捕获后
续抛出的异常并进行处理(例如:权限访问限制)。具体源码如下: 

FilterSecurityInterceptor 过滤器 

FilterSecurityInterceptor 是过滤器链的最后一个过滤器,该过滤器是过滤器链
的最后一个过滤器,根据资源权限配置来判断当前请求是否有权限访问对应的资源。如果
访问受限会抛出相关异常,最终所抛出的异常会由前一个过滤器
ExceptionTranslationFilter 进行捕获和处理。具体源码如下:

需要注意,Spring Security 的过滤器链是配置在 SpringMVC 的核心组件
DispatcherServlet 运行之前。也就是说,请求通过 Spring Security 的所有过滤器,
不意味着能够正常访问资源,该请求还需要通过 SpringMVC 的拦截器链。

 SpringSecurity 请求间共享认证信息

一般认证成功后的用户信息是通过 Session 在多个请求之间共享,那么 Spring 
Security 中是如何实现将已认证的用户信息对象 Authentication 与 Session 绑定的进行
具体分析。

在前面讲解认证成功的处理方法 successfulAuthentication() 时,有以下代码: 

 查 看 SecurityContext 接 口 及 其 实 现 类 SecurityContextImpl , 该 类 其 实 就 是 对
Authentication 的封装: 

 查 看 SecurityContextHolder 类 , 该 类 其 实 是 对 ThreadLocal 的 封 装 , 存 储
SecurityContext 对象:

 SecurityContextPersistenceFilter 过滤器 

前面提到过,在 UsernamePasswordAuthenticationFilter 过滤器认证成功之
后,会在认证成功的处理方法中将已认证的用户信息对象 Authentication 封装进
SecurityContext,并存入 SecurityContextHolder。 

之后,响应会通过 SecurityContextPersistenceFilter 过滤器,该过滤器的位置
在所有过滤器的最前面,请求到来先进它,响应返回最后一个通过它,所以在该过滤器中
处理已认证的用户信息对象 Authentication 与 Session 绑定。 

认证成功的响应通过 SecurityContextPersistenceFilter 过滤器时,会从
SecurityContextHolder 中取出封装了已认证用户信息对象 Authentication 的
SecurityContext,放进 Session 中。当请求再次到来时,请求首先经过该过滤器,该过滤
器会判断当前请求的 Session 是否存有 SecurityContext 对象,如果有则将该对象取出再次
放入 SecurityContextHolder 中,之后该请求所在的线程获得认证用户信息,后续的资源访
问不需要进行身份认证;当响应再次返回时,该过滤器同样从 SecurityContextHolder 取出
SecurityContext 对象,放入 Session 中。具体源码如下:

你好:我的2025