Bootstrap

springsecurity OAuth2.0(springboot集成springsecurity 以及springcloud集成springsecurity )

基本概念

什么是认证

认证:用户认证就是判断一个用户的身份是否合法的过程,用户去访问系统资源时系统要求验证用户的身份信息,身份合法方可继续访问,不合法则访问拒绝。

什么是会话

用户认证通过后,为了避免用户的每次操作都进行认证,将用户的信息保存在会话中。会话就是系统为了保持当前登录的用户的登录状态锁提供的机制。常见的有session方式,基于token方式等
基于seession的认证方式如下图:
他的交互方式是,用户认证成功后,在服务端生成用户相关的数据保存在session(当前会话)中,发给客户端的session_id存放到cookie中,这样用户客户端请求时带上session_id就可以验证服务器端是否存在session数据,以此完成用户的合法校验,当用户退出系统或session过期销毁时,客户端的session_id也就无效了
在这里插入图片描述
基于token的方式如下图
他的交互流程是,用户认证成功后,服务端生成一个token发给客户端,客户端可以放到cookie或localStorage等存储中,每次请求时带上token,服务端收到token通过验证后即可确认用户身份
在这里插入图片描述
基于session的认证方式由Servelt规范定制,服务端要存储session信息需要占用内存资源,客户端需要支持cookie;基于token的方式则一般不需要服务端存储token,并且不限制客户端的存储方式。如今移动互联网时代更多类型的客户端需要接入系统,系统多是采用前后端分离的架构进行实现,所以基于token的方式更适合

什么是授权

授权:授权是用户认证通过根据用户的权限来控制用户访问资源的过程,拥有资源的访问权限则正常访问,没有权限则拒绝访问
为什么要授权:认证是为了保护用户身份的合法性,授权则是为了更细粒度的对隐私数据进行划分,授权是在认证通过后发生的,控制不同的用户能够访问不同的资源

授权的数据模型

授权可以简单理解为Who对What(Which)进行How操作,包括如下:

  1. Who,即主体(Subject),主体一般是指通用户,也可以是程序,需要访问系统中的资源
  2. What,即资源(Resource),如系统菜单,页面,按钮,代码方法,系统商品信息,系统订单信息等。系统菜单,页面,按钮,代码方法都属于系统功能资源,对于web系统每个功能资源通常对应一个URL;系统商品信息,系统订单信息等属于实体资源(数据资源),实体资源由资源类型和资源实例组成,比如商品信息为资源类型,商品编号001的商品属于资源实例
  3. How,权限/许可(Permission),规定了用户对资源的操作许可,权限离开资源没有意义,如用户查询权限,用户添加权限,某个代码方法的调用权限,编号为001的用户的修改权限等,通过权限可知用户对哪些资源都有哪些操作许可。
  4. 主体,资源,权限关系如下图
    在这里插入图片描述

RBAC

如何实现授权?业界通常基于RBAC实现授权

基于角色的访问控制

RABC基于角色的访问控制人(Role-Based Access Control)是按角色进行授权,比如:主体的角色为总经理可以查询企业运营报表,查询员工工资等,访问控制流程如下
在这里插入图片描述
根据上图的逻辑判断,授权代码可表示如下:

if(主体.hasRole("总经理角色id")){
	查询工资
}

如果上图查询工资所需要的角色变化为总经理和部门经理,此时就需要修改判断逻辑为“判断用户的角色是否是总经理或部门经理”,修改代码如下

if(主体.hasRole("总经理角色id") || 主体.hasRole("部门经理角色id")){
	查询工资
}

根据上面的例子发现,当需要修改角色的权限时就需要修改授权的相关代码,系统可扩展性差

基于资源的访问控制

RABC基于资源的访问控制(Resource-Based Access Control)是按资源(或权限)进行授权,比如:用户必须具有查询工资权限才可以查询员工工资等,访问控制流程如下
在这里插入图片描述
根据上图中的判断,授权代码可以表示为

if(主体.hasPermission("查询工资权限标识")){
	查询工资
}

优点:系统设计时定义好查询工资的权限标识,即使查询工资所需要的角色变化为总经理和部门经理也不需要修改授权代码,系统可扩展性强

基于Session的认证方式

认证流程

基于Session的认证方式是,用户认证成功后,在服务端生成用户相关的数据保存在session(当前会话),而发给客户端的session_id存在cookie中,这样用客户端请求时带上session_id就可以验证服务器端是否存在session数据,以此完成用户的合法校验。当用户退出系统或session过去销毁时,客户端的session_id也就无效了。下图是session认证方式的流程图:
在这里插入图片描述

基于Session的认证机制由Servlet规范定制,Servlet容器已实现,用户通过HttpSession的操作方法可实现,如下是HttpSession相关的操作API

// 获取当前HttpSession对象
HttpSession getSession(Boolean create);

// 向Session中存放对象
void setAttribute(String name,Object value);

// 从Session中获取对象
Object getAttribute(String name);

// 移除Session中对象
void removeAttribute(String name);

// 使HttpSession失效
void invalidate();

//略...

SpringSecurity应用详解

SpringSecurity介绍

SpringSecurity是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。由于他是Spring生态系统中的一员,因此他伴随整个Spring生态系统不断修正,升级,在springboot项目中加入springsecurity更是十分简单,使用springsecurity减少了为企业系统安全控制编写大量重复代码的工作

集成springboot

创建maven工程

创建maven工程security-spring-security,工程结构如下

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.web.servlet.config.annotation;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpStatus;
import org.springframework.lang.Nullable;
import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping;

public class ViewControllerRegistry {
    @Nullable
    private ApplicationContext applicationContext;
    private final List<ViewControllerRegistration> registrations = new ArrayList(4);
    private final List<RedirectViewControllerRegistration> redirectRegistrations = new ArrayList(10);
    private int order = 1;

    public ViewControllerRegistry(@Nullable ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    public ViewControllerRegistration addViewController(String urlPathOrPattern) {
        ViewControllerRegistration registration = new ViewControllerRegistration(urlPathOrPattern);
        registration.setApplicationContext(this.applicationContext);
        this.registrations.add(registration);
        return registration;
    }

    public RedirectViewControllerRegistration addRedirectViewController(String urlPath, String redirectUrl) {
        RedirectViewControllerRegistration registration = new RedirectViewControllerRegistration(urlPath, redirectUrl);
        registration.setApplicationContext(this.applicationContext);
        this.redirectRegistrations.add(registration);
        return registration;
    }

    public void addStatusController(String urlPath, HttpStatus statusCode) {
        ViewControllerRegistration registration = new ViewControllerRegistration(urlPath);
        registration.setApplicationContext(this.applicationContext);
        registration.setStatusCode(statusCode);
        registration.getViewController().setStatusOnly(true);
        this.registrations.add(registration);
    }

    public void setOrder(int order) {
        this.order = order;
    }

    @Nullable
    protected SimpleUrlHandlerMapping buildHandlerMapping() {
        if (this.registrations.isEmpty() && this.redirectRegistrations.isEmpty()) {
            return null;
        } else {
            Map<String, Object> urlMap = new LinkedHashMap();
            Iterator var2 = this.registrations.iterator();

            while(var2.hasNext()) {
                ViewControllerRegistration registration = (ViewControllerRegistration)var2.next();
                urlMap.put(registration.getUrlPath(), registration.getViewController());
            }

            var2 = this.redirectRegistrations.iterator();

            while(var2.hasNext()) {
                RedirectViewControllerRegistration registration = (RedirectViewControllerRegistration)var2.next();
                urlMap.put(registration.getUrlPath(), registration.getViewController());
            }

            return new SimpleUrlHandlerMapping(urlMap, this.order);
        }
    }
}

package com.itheima.security.springboot.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    // 定义用户信息(查询用户信息)
    @Override
    protected UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
        return manager;
    }
    // 密码编码器
    @Bean
    public PasswordEncoder passwordEncoder(){
        return NoOpPasswordEncoder.getInstance();
    }
    // 安全拦截机制

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/r/**").authenticated()
                .anyRequest().permitAll()
                .and()
                .formLogin();

    }
}

工作原理

结构总览

SpringSecurity所解决的问题就是安全访问控制,而安全访问控制功能其实就是对所有进入系统的请求进行拦截,校验每个请求是否能够访问他所期望的资源。根据前边知识的学习,可以通过Filter或AOP等技术来实现,SpringSecurity对Web资源的保护是靠Filter实现的,所以从这个Filter来入手,逐步深入SpringSecurity原理
当初始化SpringSecurity时,会创建一个名为SpringSecurityFilterChain的Servlet过滤器,类型为org.springframework.security.web.FilterChainProxy,他实现了javax.servlet.Filter,因此外部的请求会经过此类,下图是SpringSecurtiy过滤器链结构图
在这里插入图片描述
FilterChainProxy只是一个代理,真正起作用的是FiterChainProxy中SecurityFilterChain所包含的各个Filter,同时这些Filter作为Bean被Spring代理,他们是SpringSecurity的核心,各有各的职责,但他们并不直接处理用户的认证,也不直接处理用户的授权额,而是把他们交给了认证管理器(AuthenticationManager)和决策管理器(AccessDecisionManager)进行处理,下图是FilterChainProxy相关类的UML图示
在这里插入图片描述
SpringSecurity功能的实现主要是由一系列过滤器链相互配合完成
在这里插入图片描述
下面介绍过滤器链中主要的几个过滤器及其作用:

  1. SecurityContextPersistenceFilter这个Filter是整个拦截过程的入口和出口(也就是第一个和最后一个拦截器),会在请求开始时从配置好的SecurityContextRepository中获取SecurityContext,然后把它设置给SecurityContextHolder。在请求完成后将SecurityContextHolder持有的SecurityContext再保存到配置好的SecurityContextRepository,同时清除SecurityContextHolder所持有的SecurityContext
  2. UsernamePasswordAuthenticationFilter用于处理来自表单提交的认证。该表单必须提供对应的用户名和密码,其内部还有登录成或失败后进行处理的AuthenticationSuccessHandler和AuthenticationFailureHandler,这些都可以根据相关需求作出改编
  3. FilterSecurityInterceptor是用于保护web资源的,使用AccessDecisionManager对当前用户进行授权访问,前面已经详细介绍过了
  4. ExceptionTranslationFilter能够捕获来自FilterChain所有的异常,并进行处理。但是他只会处理两类异常:AuthenticationException和AccessDeniedException,其他的异常会相继抛出

认证流程

UserDetailService

  1. 现在我们知道DaoAuthenticationProvider处理web表单的认证逻辑,认证成功后既得到一个Authentication(UsernamePasswordAuthenticationToken实现),里面包含了身份信息(Principal)。这个身份信息就是一个Object,大多数情况下可以被强制转化为UserDetails对象。
  2. DaoAuthenticationProvider中包含了一个UserDetailsService实力,他负责根据用户名提取用户信息UserDetails(包含密码),而后DaoAuthenticationProvider会去对比UserDetailsService提取的用户密码和用户提交的密码是否匹配作为认证成功的关键依据,因此可以通过将自定义的UserDetailsService公开为spring bean来定义自定义身份验证
public interface UserDetailsService{
	UserDetails loadUserByUsername(String username) throw UsernameNotFoundException;
}
  1. 很多人把DaoAuthenticationProvider和UserDetailsService的职责搞混淆,其实UserDetailsService只负责从特定的地方(通常是数据库)加载用户信息,仅此而已。而DaoAuthenticationProvider的职责更大,他完成完整的认证流程,同时会把UserDetails填充至Authentication
  2. 上面一直提到UserDetails是用户信息,咱们看一下他的真面目
public interface UserDetails extends Serializable{
	Collection<? extends GrantedAuthority> getAuthorities();

	String getPassword();

	String getUsername();

	boolean isAccountNonExpired();

	boolean isAccountNonLocked();

	boolean isCredentialsNonExpired();

	boolean isEnabled();
}
  1. 他与Authentication接口很类似,比如他们都拥有username,authorities。Authentication的isCredentials()与UserDetails中的getPassword()需要被区分对待,前者是用户提交的密码凭证,后者是用户实际存储的密码,认证其实就是这两者的比对。Authencation中的getAuthorities()实际是由UserDetails的getAuthorties()传递而形成的。还记得Authentication的getDetails()方法吗?其中的UserDetails用户详细信息便是经过了AuthenticationProvider认证之后被填充的。
  2. 通过实现UserDetailsService和UserDetails,我们可以完成对用户信息获取方式以及用户信息字段的扩展
  3. SpringSecurity提供的InMemoryUserDetailsManager(内存认证),JdbcUserDetailsManager(jdbc认证)就是UserDetailsService的实现类,主要区别无非就是从内存还是从数据库加载用户
测试

自定义UserDetailsService

@Service
public class SpringDataUserDetailService implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 从数据库进行查找的操作
        return null;
    }
}

PasswordEncoder

  1. 认识PasswordEncoder
    DaoAuthenticationProvider认证处理器通过UserDetailsService获取到UserDetails后,他是如何与请求Authentication中的密码做对比呢?
    在这里Spring Security为了适应多种多样的加密类型,又做了抽象,DaoAuthenticationProvider通过PasswordEncoder接口的matches方法进行密码的对比,而具体的密码对比细节取决于实现
public interface PasswordEncoder{
	String encode(CharSequence var1);

	boolean matches(CharSequence var1,String var2);

	default boolean ungradeEncoding(String encodedPassword){
		return false;
	}
}
  1. 而Spring Security提供很多内置的PasswordEncoder,能够开箱即用,使用某种PasswordEncoder只需要进行如下声明即可,如下;
@Bean
public PasswordEncoder passwordEncoder(){
	return NoOpPasswordEncoder.getInstance();
}
  1. NoOpPasswordEncoder采用字符串匹配算法,不对密码进行加密处理,密码比较流程如下:
    1. 用户输入密码(明文)
    2. DaoAuthenticationProvider获取UserDetails(其中存储了用户的正确密码)
    3. DaoAuthenticationProvider使用PasswordEncoder对输入的密码和正确的密码进行校验,密码一致则校验通过,否则校验失败
    4. 其中,NoOpPasswordEncoder的校验规则拿输入的密码和UserDetails中的正确密码进行字符串比较,字符串内容一致则通过,否则校验失败
    5. 实际项目中推荐使用其他算法
  2. 使用BCryptPasswordEncoder
    1. 配置BCryptPasswordEncoder
      在安全配置类中定义
@Bean
public PasswordEncoder passwordEncoder(){
	return new BCrpytPasswordEncoder();
}

授权流程

授权流程

SpringSecurity可以通过http.authorizeRequests()对web请求进行授权保护。SpringSecurity使用标准Filter建立了对web请求的拦截,最终实现对资源的授权访问
SpringSecurity的授权流程如下
在这里插入图片描述

自定义登录页面

连接数据库认证

前边的例子我们是将用户信息存储在内存中,实际项目中用户信息存储在数据库中,本节实现从数据库读取用户信息。根据前边对认证流程研究,只需要重新定义UserDetailService即可实现根据用户账号查询数据库

代码实现

@Service
public class SpringDataUserDetailService implements UserDetailsService {
    @Autowired
    private UserMapper userMapper;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 从数据库进行查找的操作
        User user = userMapper.selectUserByUsername(username);
        if(user == null){
            // 如果用户查不到,返回null,由Provider抛出异常
            return null;
        }
        UserDetails userDetails = org.springframework.security.core.userdetails.User
        .withUsername(user.getUsername()).password(user.getPassword()).authorities("p1").build();
        return userDetails;
    }
}

会话

用户认证通过后,为了避免用户的每次操作都进行认证,将用户的信息保存在会话中。SpringSecurity提供会话管理,认证通过后将身份信息放入SecurityContextHolder上下文,SecurityContext与当前线程进行绑定,方便获取用户身份

获取用户身份

private String getUsername(){
        String username = null;
        // 当前认证通过的用户身份
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        // 用户身份
        Object principal = authentication.getPrincipal();
        if(principal == null){
            username = "匿名";
        }
        if(principal instanceof org.springframework.security.core.userdetails.UserDetails){
            UserDetails userDetails = (UserDetails) principal;
            username = userDetails.getUsername();
        }else{
            username = principal.toString();
        }
        return username;
    }

会话控制

我们可以通过一下选项准确控制会话何时创建以及SpringSecurity如何与之交互

  1. always:如果没有session就创建一个
  2. ifRequired:如果需要就创建一个session(默认)登录时
  3. never:SpringSecurity将不会创建session,但是如果应用中其他地方创建了session,那么SpringSecurity将会使用他
  4. stateless:SpringSecurity将绝对不会创建session,也不会试用他
    通过以下控制方式对该选项进行配置
@Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable() //取消跨站伪造
                .authorizeRequests()
                .antMatchers("/r/**").authenticated()
                .anyRequest().permitAll()
                .and()
                .formLogin()
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);
    }

默认情况下,SpringSecurity会为每个登录成功的用户创建一个Session,就是ifRequired
若选用never,则只是SpringSecurity对登录成功的用户不创建Session了,但若你的应用程序在某地方新建了Session,那么SpringSecurity会用他的
若使用stateless,则说明SpringSecurity对登录成功的用户不会创建Session了,你的应用程序也不会允许创建session。并且他会暗示不适用cookie,所以每个请求都需要重新进行身份验证。这种无状态架构适用于REST_API及其无状态认证机制

会话超时

可以在Servlet容器中设置Session的超时时间,如下设置Session的有效期是3600s
springboot配置文件:

server.servlet.session.timeout = 3600s

session超时之后,可以通过SpringSecurity设置跳转的路径

http.sessionManagement()
	.expireUrl("/login-view?error=EXPIRED_SESSION")
	.invalidSessionUrl("/login-view?error=INVALID_SESSION");

expired指session过期,invalidSession指传入的sessionid无效

退出

可以自定义退出成功的页面也url

,and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/login-view?logout");

当退出操作发生时,将发生

  1. 使得HTTP session无效
  2. 清楚SecurityContextHolder
  3. 跳转到/login-view?logot
    但是类似于配置登录功能,咱们可以进一步自定义退出功能

授权

授权

授权的方式包括web授权和方法授权,web授权是指通过url进行拦截,方法授权是通过方法拦截进行授权。他们都会调用accessDecisionManager进行授权决策,若为web授权则拦截器为Filter SecurityInterceptor;若为方法授权则拦截器为MethodSecurityInterceptor。如果同时进行web授权和方法授权则优先执行web授权,再执行方法授权,最后决策通过,则允许访问资源,否则禁止访问
类关系如下:
在这里插入图片描述

从数据库查询用户权限

@Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 从数据库进行查找的操作
        User user = userMapper.selectUserByUsername(username);
        if(user == null){
            // 如果用户查不到,返回null,由Provider抛出异常
            return null;
        }
        // 根据用户ID查询用户权限
        List<Permission> permissions = userMapper.findPermissionByUserId(user.getId());
        // 将Permission转成数组
        String[] permissionArray = new String[permissions.size()];
        permissions.toArray(permissionArray);
        UserDetails userDetails = org.springframework.security.core.userdetails.User.
                withUsername(user.getUsername()).password(user.getPassword()).authorities(permissionArray).build();
        return userDetails;
    }

方法授权

我们可以在任何@Configuration实例上使用@EnableGlobalMethodSecurity注释来启用基于注解的安全性
以下内容将启用SpringSecurity的@Secured注释

@EnableGlobalMethodSecurity(securedEnabled = true)
public class MethodSecurityConfig { //... }

然后向方法(在类或接口上)添加注解就会限制对该方法的访问。SpringSecurity的原生注释支持为该方法定义了一组属性。这些将被传给AccessDecisionManager以供他作出实际的决定:

public interface BankService{
	@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
	public Account readAccount(Long id);

	@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
	public Account[] findAccounts();

	@Secured("ROLE_TELLER")
	public Account post(Account account,double amount);
}

以上配置表明readAccount,findAccounts方法可匿名访问,底层使用WebExpressionVoter投票器,可以AffirmativeBased第23行代码追踪
post方法需要有TELLER角色才能访问,底层使用RoleVoter投票器
使用如下代码可以使用prePost注解的支持

@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig{
	//...
}

相应java代码如下

public interface BankService{
	@preAuthorize("isAnonymous()")
	public Account readAccount(Long id);

	@preAuthorize("isAnonymous()")
	public Account[] findAccounts();

	@preAuthorize("hasAuthority('p_transfer') and hasAuthority('p_read_account')")
	public Account post(Account account,double amount);
}

以上配置表明readAccount,findAccounts方法可匿名访问,post方法需要同时拥有p_transfer和p_read_account权限才能访问,底层使用WebExpressionVoter投票器,可以AffirmativeBased第23行代码追踪

分布式系统认证方案

分布式认证需求

分布式系统的每个服务都会有认证,授权的需求,如果每个服务都实现一套认证授权逻辑会显非常冗余,考虑分布式系统共享性的特点,需要由独立的认证服务处理系统认证授权的请求;考虑分布式系统开放性的特点,不仅对系统内部服务提供认证,对第三方系统也要提供认证。分布式认证的需求如下:

  1. 统一认证授权
    1. 提供独立的认证服务,统一处理认证授权
    2. 无论是不同类型的用户,这是不同种类的客户端(web端,H5,APP),均采用一致的认证,权限,会话机制,实现统一认证授权
    3. 要实现统一则认证方式必须可扩展,支持各种认证需求,比如:用户名密码认证,短信验证码,二维码,人脸识别等认证方式,并可以非常灵活的切换
  2. 应用纳入认证
    1. 应提供扩展和开放能力,提供安全的系统对接机制,并可开放部分API给接入第三方使用,一方应用(内部系统服务)和三方应用(第三方应用)均采用统一机制纳入

选型分析

基于session的认证方式

在分布式的环境下,基于session的认证会出现一个问题,每个应用服务都需要在session中存储用户身份信息,通过负载均衡将本地的请求分配到另一个应用服务器需要将session信息带过去,否则会重新认证
在这里插入图片描述

这个时候,通常的做法有下面几种:

  1. Session复制:多台应用服务器之间同步session,使session保持一致,对外透明
  2. Session黏贴:当用户访问集群中某台服务器后,强制指定后续所有请求均落到此机器上
  3. Session集中存储:将Session存入分布式缓存中,所有服务器应用实例统一从分布式缓存中存取Session
    总体来讲,基于session的认证的认证方式,可以更好的在服务端对会话进行控制,且安全性较高,但是,session方法基于cookie,在复杂多样的移动客户端上不能有效的使用,并且无法跨域,另外随着系统的扩展需提高session的复制,黏贴以及存储的容错性

基于token的存储方式

基于token的存储方式,服务端不用存储认证数据,易维护扩展性强,客户端可以把token存在任意地方,并且可以实现web和app统一认证机制。其缺点也很明显,token由于自包含信息,因此一般数据量较大,而且每次请求都需要传递,因此比较占带宽。另外,token的签名验证操作也会给cpu带来额外的负担
在这里插入图片描述

技术方案

根据选型的分析,决定采用基于token的认证方式,它的优点是:

  1. 适合统一认证的机制,客户端,一方应用,三方应用都遵循一致的认证机制
  2. token认证方式对第三方应用接入更适合,因为他更开放,可使用当前有流行的开放协议Oauth2.0,JWT等
  3. 一般情况下服务端无需存储会话信息,减轻了服务端的压力
    分布式认证方案如下图
    在这里插入图片描述

OAuth2.0

介绍

OAuth2.0(开放授权)是一个开放标准,允许用户授权第三方应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方应用或分享他们数据的所有内容。OAuth2.0是OAuth协议的延续版本,但不向后兼容OAuth1.0。很多大公司如Google,Yahoo,Microsoft等都提供了OAUTH认证服务,这些都说明OAUTH标准逐渐成为开放资源授权的标准
OAuth2.0包括以下角色

  1. 客户端
    本身不存储资源,需要通过资源拥有者的授权去请求资源服务器的资源,比如安卓客户端,web客户端,微信客户端等
  2. 资源拥有者
    通常为用户,也可以是应用程序,即该资源的拥有者。
  3. 授权服务器(也称认证服务器)
    用于服务提供商对资源拥有的身份进行认证,对访问资源进行授权,认证成功后会给客户端发放令牌啊(access_token),作为客户端访问资源服务器的凭据
  4. 资源服务器
    存储资源的服务器
  5. 现在还有一个问题,服务提供商能允许随便一个客户端就接入到他的授权服务器吗?答案是否定的,服务提供商会给准入的接入方一个身份,用于接入时的凭据:
    client_id:客户端表示
    client_secret:客户端密钥
    因此准确的说,授权服务器对两中OAuth2.0中的两个角色进行认证授权,分别是资源拥有者客户端

Spring Cloud Security OAuth2.0

环境介绍

  1. Spring Security-OAuth2是对OAuth2的一种实现,并且跟我们之前学习的SpringSecurity相辅相成,与Spring Cloud体系的集成也非常便利,接下来,我们需要对他进行学习,最终使用它来实现我们设计的分布式认证授权方案
  2. OAuth2.0的服务提供方涵盖两个服务,即授权服务(Authorization Server,也叫认证服务)和资源服务(Resource Server),使用SpringSecurity OAuth2的时候你可以选择把他们在同一个应用程序中实现,也可以选择建立使用同一个授权服务的多个资源服务
  3. **授权服务(Authorization Server)**应包含对接入端以及登入用户的合法性进行验证并且颁发token等功能,对令牌的请求端点由SpringMVC控制器进行实现,下面是配置一个认证服务必须要实现的endpoints:
    1. AuthorizationEndpoint服务于认证请求,默认URL:/oauth/authorize
    2. TokenEndpoint服务于访问令牌的请求:默认URL:/oauth/token
      资源服务(Resource Server),应包含对资源的保护功能,对非法请求进行拦截,对请求中token进行解析鉴权等,下面的过滤器用于实现OAuth2.0资源服务:
    3. OAuth2AuthenticationProcessingFilter用来对请求给出的身份令牌解析鉴权。
      本教程分别创建uaa授权服务(也叫认证服务)和order订单资源服务
      在这里插入图片描述
      认证流程如下:
  4. 客户端请求UAA授权服务进行认证
  5. 认证通过后由UAA颁发令牌
  6. 客户端携带令牌Token请求资源服务
  7. 资源服务校验令牌的合法性,合法即返回资源信息

环境搭建

父工程依赖

<dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Greenwich.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>javax.servlet</groupId>
                <artifactId>javax.servlet-api</artifactId>
                <version>3.1.0</version>
                <scope>provided</scope>
            </dependency>
            <dependency>
                <groupId>javax.interceptor</groupId>
                <artifactId>javax.interceptor-api</artifactId>
                <version>1.2</version>
            </dependency>
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>fastjson</artifactId>
                <version>1.2.47</version>
            </dependency>
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>1.18.8</version>
            </dependency>
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>5.1.47</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.security</groupId>
                <artifactId>spring-security-jwt</artifactId>
                <version>1.0.10.RELEASE</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.security.oauth.boot</groupId>
                <artifactId>spring-security-oauth2-autoconfigure</artifactId>
                <version>2.1.3.RELEASE</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

子工程依赖

 <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>com.netflix.hystrix</groupId>
            <artifactId>hystrix-javanica</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.retry</groupId>
            <artifactId>spring-retry</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-commons</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-jwt</artifactId>
        </dependency>
        <dependency>
            <groupId>javax.interceptor</groupId>
            <artifactId>javax.interceptor-api</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

项目整体结构
在这里插入图片描述

授权服务器配置

EnableAuthorizationServer

可以用@EnableAuthorizationServer注解并继承AuthorizationServerConfigurerAdapter来配置OAuth2.0服务器
在Config创建AuthorizationServer

@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {
    
}

AuthorizationServerConfigurerAdapter要求配置以下几个类,这几个类是由Spring创建的独立的配置对象,他们会被Spring传入AuthorizationServerConfigurer中进行配置


  1. ClientDetailsServiceConfigurer:用来配置客户端详情服务(ClientDetailsService),客户端详情信息在这里进行初始化,你能够把客户端详情信息写死在这里或者是通过数据库来存储调取详情信息
  2. AuthorizationServerEndpointsConfigurer:用来配置令牌(token)的访问端点和令牌服务(tokenservices)
  3. AuthorizationServerSecurityConfigurer:用来配置令牌端点的安全约束

配置客户端详细信息

ClientDetailsServicConfigurer能够使用内存或者JDBC来实现客户端详情服务(CilentDetailsService),ClientDetailsService负责查找ClientDetails,而ClientDetails有几个重要的属性如下列表:

  1. clientid:(必须的)用来表示客户的id
  2. secret:(需要值得信任的客户端)客户端安全码,如果有的话
  3. scope:用来限制客户端的访问范围,如果为空(默认)的话,那么客户端就拥有全部的访问范围
  4. authorizedGrantTypes:此客户端可以使用的授权类型,默认为空
  5. authorities:此客户端可以使用的权限(基于Spring Security authorities)
    客户端详情(Client Details)能够在应用程序运行的时候进行更新,可以通过访问底层的存储服务(例如将客户端详情存储在一个关系数据库的表中,就可以使用JdbcClientDetailsService)或者通过自己实现ClientRegistrationService接口(同时你可以实现ClientDetailsService接口)来进行管理。
    我们暂时使用内存方式存储客户端信息,配置如下
// 配置客户端详细信息服务
    @Override
    public void configure(ClientDetailsServiceConfigurer clientDetailsServiceConfigurer) throws Exception {
        // 暂时使用内存方式
        clientDetailsServiceConfigurer.inMemory() // 使用in_memory存储
                .withClient("c1") // client_id
                .secret(new BCryptPasswordEncoder().encode("secret")) //客户端密钥
                .resourceIds("res1") // 资源列表
                .authorizedGrantTypes("authorization_code","password","client_credenttials","implicit","refresh_token") // 该client允许的授权类型"authorization_code","password","client_credenttials","implicit","refresh_token"
                .scopes("all") // 允许的授权范围
                .autoApprove(false) // false跳转到授权的页面
                .redirectUris("http://www.baidu.com"); // 回调地址
    }

管理令牌

  1. AuthorizationServerTokenServices接口定义了一些操作使得你可以对令牌进行一些必要的管理,令牌可以被用来加载身份信息,里面包含了这个令牌的相关权限
  2. 自己可以创建AuthorizationServerTokenServices这个接口的实现,则需要继承DefaultTokenServices这个类,里面包含了一些有用实现,你可以使用它来修改令牌的格式和令牌的存储。默认的,当他尝试创建一个令牌的时候,是使用随机值来进行填充的,除了持久化令牌是委托一个TokenStore接口来实现以外,这个类几乎帮你做了所有的事情。并且TokenStore这个接口有一个默认的实现,他就是InMemoryTokenStore,如其命名,所有的令牌是被保存在了内存中。除了使用这个类以外,你还可以使用一些其他的预定义实现,下面有几个版本,他们都实现了TokenStore接口:
    1. InMemoryTokenStore:这个版本的实现是被默认采用的,他可以完美的工作在单服务器上(即访问并发压力不大的情况下,并且他在失败的时候不会进行备份),大多数的项目都可以使用这个版本的实现来进行尝试,你可以在开发的时候使用它来进行管理,因为不会被保存到磁盘中,所以会更容易调试
    2. JdbcTokenStore:这是一个基于JDBC的实现版本,令牌会被保存进关系型数据库。使用这个版本的实现时,你可以在不同的服务器之间共享令牌信息,使用这个版本的时候请注意把“spring jdbc”这个依赖加入到你的classpath中
    3. JwtTokenStore:这个版本的全称是JSON Web Token(JWT),他可以把令牌相关的数据进行编码(因此对于后端服务来说,他不需要进行存储,这将是一个重大优势),但是他有一个缺点,那就是撤销一个已经授权的令牌将会非常困难,所以他通常用来处理一个生命周期较短的令牌以及撤销刷新令牌

定义TokenConfig

在config包下定义TokenConfig,我们暂时使用InMemoryTokenStore,生成一个普通的令牌

@Configuration
public class TokenConfig {
	// 令牌存储策略
    @Bean
    public TokenStore tokenStore(){
    	// 内存方式,生成普通令牌
        return new InMemoryTokenStore();
    }
}

定义AuthorizationServerTokenServices

在AuthorizationServer中定义AuthorizationServerTokenServices

	@Autowired
    private TokenStore tokenStore;
    @Autowired
    private ClientDetailsService clientDetailsService;
	@Bean
    public AuthorizationServerTokenServices tokenServices(){
        DefaultTokenServices service = new DefaultTokenServices();
        service.setClientDetailsService(clientDetailsService); // 客户端信息服务
        service.setSupportRefreshToken(true); // 是否刷新产生令牌
        service.setTokenStore(tokenStore); //令牌存储策略
        service.setAccessTokenValiditySeconds(7200); //令牌默认有效期两小时
        service.setRefreshTokenValiditySeconds(259200); // 刷新令牌默认有效期三天
        return service;
    }

令牌访问端点配置

AuthorizationServerEndpointsConfigurer这个对象的实例可以完成令牌服务以及令牌的endpoint配置

配置授权类型(Grant Types)

AuthorizationServerEndpointsConfigurer通过设定以下属性决定支持的授权类型(Grant Types):

  1. authenticationManager:认证管理器,当你选择了资源所有者密码(password)授权类型的时候,请设置这个属性注入一个AuthenticationManager对象
  2. userDetailsService:如果你设置了这个属性的话,那说明你有一个自己的UserDetailsService接口的实现,或者你可以把这个东西设置到全局域上面去(例如GlobalAuthenticationManagerConfigurer这个配置对象),当你设置了这个之后,那么"refresh_token"即刷新令牌授权类型模式的流程中就会包含一个检查,用来确保这个账号是否仍然有效,假如说你禁用了这个账户的话
  3. authorizationCodeServices:这个属性是用来授权码服务的(即AuthorizationCodeServices的实例对象),主要用于"authorization_code"授权码类型模式
  4. implicitGrantService:这个属性用于设置隐式授权模式,用来管理隐式授权模式的状态
  5. tokenGranter:当你设置了这个东西(即ToeknGranter接口实现),那么授权将会交由你来完全掌控,并切会忽略掉上面的这几个属性,这个属性一般是用作扩展用途的,即标准的四种授权模式已经满足不了你的需求的时候,才会考虑使用这个

配置授权端点的URL(Endpoints URLs):

AuthorizationServerEndpointsConfigurer这个配置对象有一个叫做pathMapping()的方法用来配置端点URL链接,他有两个链接

  1. 第一个参数:String类型的,这个端点URL的默认链接
  2. 第二个参数:String类型的,你要进行替代的URL连接
    以上的参数都将以“/”字符为开始的字符串,框架的默认URL连接如下,可以作为pathMapping()方法的第一个参数:
/ oauth/authorize:授权端点
/oauth/token:令牌端点
/oauth/confirm_access:用户确认授权提交端点
/oauth/error:授权服务错误信息端点
/oauth/check_token:用于资源服务访问的令牌解析端点	
/oauth/token_key:提供公有密匙的端点,如果你使用JWT令牌的话

需要注意的是授权端点这个URL应该被SpringSecurity保护起来只供授权用户访问
在AuthorizationServer配置令牌访问端点

// 令牌访问端点
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer authorizationServerEndpointsConfigurer) throws Exception {
        authorizationServerEndpointsConfigurer
                .authenticationManager(authenticationManager) // 密码模式需要
                .authorizationCodeServices(authorizationCodeServices) // 授权模式需要
                .tokenServices(tokenServices()) // 令牌管理服务
                .allowedTokenEndpointRequestMethods(HttpMethod.POST); // 允许post提交
    }

令牌端点的安全约束

AuthorizationServerSecurityConfigurer:用来配置令牌端点(Token Endpoint)的安全约束,在AuthorizationServer中配置如下

 // 令牌访问端点安全策略
    @Override
    public void configure(AuthorizationServerSecurityConfigurer authorizationServerSecurityConfigurer) throws Exception {
        authorizationServerSecurityConfigurer
                .tokenKeyAccess("permitAll()") // /oauth/token_key公开
                .checkTokenAccess("permitAll()") // /oauth/check_token公开
                .allowFormAuthenticationForClients(); // 表单认证,申请令牌
    }

授权码模式

授权码模式介绍

下图是授权码模式交互图:
在这里插入图片描述

  1. 资源拥有者打开客户端,客户端要求资源拥有者给予授权,它将浏览器被重定向到授权服务器,重定向时会附加客户端的身份信息。如:
/uaa/oauth/authorize?client_id=c1&response_type=code&scope=all&redirect_uri=http://www.baidu.com

参数列表如下

client_id:客户端准入标识
response_type:授权码模式固定为code
scope:客户端权限
redirect_uri:跳转uri,当授权码申请成功后会跳转到此地址,并在后边带上code参数(授权码)
  1. 浏览器出现向授权服务器授权页面,之后将用户同意授权
  2. 授权服务器将授权码(AuthorizationCode)转经浏览器发送给client(通过redirect_uri)
  3. 客户端拿着授权码向授权服务器索要访问access_token,请求如下
/uaa/aouth/token?client_id=c1&client_secret=secret&grant_type=authorization_code&code=5PgfcD&redirect_uri=http://www.baidu.com

参数列表如下

client_id:客户端准入标识
client_secret:客户端密钥
grant_type:授权类型,填写authorization_code,表示授权码模式
code:授权码
redirect_uri:申请授权码时的跳转url,一定和申请授权码时用的redirect_uri一致
  1. 授权服务器返回令牌(access_token)
    这种模式是四种模式中最安全的一个模式。一般用于client是Web服务器端应用或第三方的App调用资源服务的时候。因为在这种模式中access_token不会经过浏览器或移动端的App,而是直接从服务端去交换,这样就最大限度的减小了令牌泄露的风险

简化模式

下图是简化模式交互图:
在这里插入图片描述

  1. **资源拥有者打开客户端,客户端要求资源拥有者给予授权,他将浏览器被重定向到授权服务器,重定向时会附加客户端的身份信息,如:
/uaa/oauth/aotuhorize?client_id=c1&response_type=token&scope=all&redirect_uri=http://www.baidu.com

参数参考上文
2. 浏览器出现向授权服务器授权页面,之后将同意用户授权
3. **授权服务器将授权码令牌(access_token)以Hash的形式存放在重定向的uri的fargment中发送给浏览器
注:fragment主要是用来表示URI所标识资源里的某个资源,在URI的末尾通过(#)作为fragment的开头,其中#不属于fragment的值。如https://domain/index#L18这个URI中L18就是fragment的值。大家只需要知道js通过浏览器地址栏变化的方式能获取到fragment就行了
一般来说,简化模式用于没有服务器端的第三方单页面应用,因为没有服务器就无法接受授权码

密码模式

下图是密码模式交互图:
在这里插入图片描述

  1. 资源拥有者将用户名,密码发送给客户端
  2. **客户端拿着资源拥有者的用户名,密码向授权服务器请求令牌(access_token),请求如下:
/uaa/oauth/token?client_id=c1&client_secret=secret&grant_type=password&scope=all&username=zhangsan&password=123

参数列表如下

client_id:客户端准入标识
client_secret:客户端密钥
grant_type:授权类型,填写password,表示密码模式
username:资源拥有者用户名
password:资源拥有者密码
  1. **授权服务器将令牌(access_token)发送给client
    这种模式十分简单,但是却意味着直接将用户敏感信息泄露给了client,因此这就说明这种模式只能用于client使我们自己开发的情况下。因此密码模式一般用于我们自己开发的,第一方原生App或第一方单页面应用

客户端模式

  1. 客户端向授权服务器发送自己的身份信息,并请求令牌(access_token)
  2. 确认客户端身份无误后,将令牌(access_token)发给client,请求如下
/uaa/oauth/token?client_id=c1&client_secret=secret&grant_type=client_credentials

参数列表如下

client_id:客户端准入标识
client_secret:客户端密钥
grant_type:授权类型,填写client_credentials,表示客户端模式

这种模式是最方便但是是最不安全的方式。因此这就要求我们对client完全信任,而client本身也是安全的。因此这种模式一般用来提供我们完全信任的服务器端服务。比如,合作方系统对接,拉取一组用户信息

资源服务测试

资源服务器配置

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    public static final String RESOURCE_ID = "res1";

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.resourceId(RESOURCE_ID)
                .tokenServices(tokenService()) //验证令牌的服务
                .stateless(true);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/**").access("#oauth.hasScope('all')")
                .and().csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }
}

验证token

ResourceServerTokenServices是组成授权服务的另一半,如果你的授权服务和资源服务在同一个应用程序上的话,你可以使用DefaultTokenServices珍,这样的话,你就不用考虑关于实现所有必要的接口的一致性的问题。如果你的资源服务器是分离开的话,那么你就必须要确保能够有匹配授权服务器提供的ResourceServerToeknServices,他知道如何对令牌进行解码
令牌解析方法:

  1. 使用DefaultTokenServices在资源服务器本地配置令牌存储,解码,解析方式
  2. 使用RemoteTokenServices资源服务器通过HTTP请求来解码令牌,每次都请求授权服务器端点/oauth/check_token
    使用授权服务的/oauth/check_token端点你需要在授权服务将这个端点暴露出去,以便资源服务可以进行访问,这在咱们授权配置服务中已经提到了,下面是一个例子,在这个例子中,我们在授权服务中配置了/oauth/check_token和/oauth/token_key这两个端点:
	 //资源服务令牌解析服务
    @Bean
    public ResourceServerTokenServices tokenService(){
        // 使用远程服务请求授权服务器校验token,必须指定校验token的url,client_id,client_secret
        RemoteTokenServices service = new RemoteTokenServices();
        service.setCheckTokenEndpointUrl("http://localhost:8083/uaa/oauth/check_token");
        service.setClientId("c1");
        service.setClientSecret("secret");
        return service;
    }
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    // 安全拦截机制

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/r/**").authenticated()
                .anyRequest().permitAll();
    }
}

JWT令牌

JWT令牌介绍

当资源服务和授权服务不在一起的时候资源服务使用RemoteTokenServices远程请求授权服务验证token,如果访问量较大将会影响系统的性能。
解决上面的问题
令牌采用JWT格式即可解决上面的问题,用户认证通过会得到一个JWT令牌,JWT令牌中已经包括了用户相关的信息,客户端只需要携带JWT访问资源服务,资源服务根据实现约定的算法自行完成令牌校验,无需每次都请求认证服务完成授权

什么是JWT

JSON Web Token(JWT)是一个开放的行业标准,他定义了一种简介的,自包含的协议格式,用于在通信双方传输json对象,传递的信息经过数字签名可以被验证和信任。JWT可以使用HMAC算法或使用RSA的公钥/私钥对来签名,防止被篡改
JWT令牌的优点

  1. jwt基于json,非常方便解析
  2. 可以在令牌中自定义丰富的内容,易扩展
  3. 通过非对称加密算法以及数字签名技术,JWT防止篡改,安全性高
  4. 资源服务使用JWT可不依赖认证服务即可完成授权
    缺点
  5. JWT令牌较长,占存储空间较大

JWT令牌结构

JWT令牌由三部分组成,每部分中间使用(.)分割,比如xxxxx.yyyyy.zzz

  1. Header
    头部包括令牌的类型(即JWT)以及使用的Hash算法,一个例子如下
{
	alg:"HS256",
	typ:"JWT"
}

将上边的内容使用Base64Url编码,得到一个字符串就是JWT令牌的第一部分
2. Payload
第二部分是负载,内容也是一个json对象,他是存放信息的地方,他可以存放jwt提供的现成字段,比如:iss(签发者),exp(过期时间戳),sub(面向的用户等),也可自定义字段
此部分不建议存放敏感信息,因为此部分可以解码还原原始内容
最后将第二部分负载使用Base64Url编码,得到的一个字符串就是JWT令牌的第二部分,一个例子如下

{
	"sub":"1234567890",
	"name":456,
	"admin":true
	
}
  1. Signature
    第三部分是签名,此部分用于防止jwt内容被篡改
    这个部分使用base64url将前两部分进行编码,编码后使用(.)连接组成字符串,时候使用header中声明的签名算法进行签名,一个例子如下
HMACSHA256(
	base64UrlEncode(header) + "." +
	base64UrlEncode(payload),
	secret)
)
base64UrlEncode(header) :jwt令牌的第一部分
base64UrlEncode(payload):jwt令牌的第二部分
secret:签名所使用的密钥

配置JWT令牌服务

在uaa中配置jwt令牌服务,即可实现生成jwt格式的令牌
TokenConfig

@Configuration
public class TokenConfig {
    private String SIGNING_KEY = "uaa123";

    @Bean
    public JwtAccessTokenConverter accessTokenConverter(){
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(SIGNING_KEY); // 对称密钥,资源服务器使用该密钥来验证
        return converter;
    }
    // 令牌存储策略
    @Bean
    public TokenStore tokenStore(){
        return new JwtTokenStore(accessTokenConverter());
    }
//    @Bean
//    public TokenStore tokenStore(){
//        // 内存方式,生成普通令牌
//        return new InMemoryTokenStore();
//    }
}

定义JWT令牌服务

	@Bean
    public AuthorizationServerTokenServices tokenServices(){
        DefaultTokenServices service = new DefaultTokenServices();
        service.setClientDetailsService(clientDetailsService); // 客户端信息服务
        service.setSupportRefreshToken(true); // 是否刷新产生令牌
        service.setTokenStore(tokenStore); //令牌存储策略
        // 令牌增强
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        tokenEnhancerChain.setTokenEnhancers(Arrays.asList(accessTokenConverter));
        service.setTokenEnhancer(tokenEnhancerChain);
        
        service.setAccessTokenValiditySeconds(7200); //令牌默认有效期两小时
        service.setRefreshTokenValiditySeconds(259200); // 刷新令牌默认有效期三天
        return service;
    }

校验jwt令牌

资源服务需要和授权服务拥有一致的签字,令牌服务等:

  1. 将授权服务中的TokenConfig类拷贝到资源服务中
  2. 屏蔽资源服务原来的令牌服务类
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    public static final String RESOURCE_ID = "res1";
    @Autowired
    private TokenStore tokenStore;
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.resourceId(RESOURCE_ID)
                .tokenStore(tokenStore)
                //.tokenServices(tokenService()) //验证令牌的服务
                .stateless(true);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/**").access("#oauth.hasScope('all')")
                .and().csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }
    //资源服务令牌解析服务
//    @Bean
//    public ResourceServerTokenServices tokenService(){
//        // 使用远程服务请求授权服务器校验token,必须指定校验token的url,client_id,client_secret
//        RemoteTokenServices service = new RemoteTokenServices();
//        service.setCheckTokenEndpointUrl("http://localhost:8083/uaa/oauth/check_token");
//        service.setClientId("c1");
//        service.setClientSecret("secret");
//        return service;
//    }
}

SpringSecurity实现分布式系统授权

需求分析

回顾技术方案如下
在这里插入图片描述

  1. UAA认证服务负责认证授权
  2. 所有请求经过网关到达微服务
  3. 网关负责鉴权客户端以及请求转发
  4. 网关将token解析后传给微服务,微服务进行授权

注册中心

所有微服务的请求都经过网关,网关从注册中心读取微服务的地址,将请求转发至微服务

<dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
    </dependencies>
@SpringBootApplication
@EnableEurekaServer
public class DiscoveryServer {
    public static void main(String[] args) {
        SpringApplication.run(DiscoveryServer.class, args);
    }
}

网关

网关整合OAuth2.0有两种思路,一种是认证服务器生成jwt令牌,所有请求统一在网关层验证,判断权限等操作;另一种是由各资源服务处理,网关只做请求转发
我们选用第一种。我们把API网关作为OAuth2.0的资源服务器角色,实现接入客户端权限拦截,令牌解析并转发当前登录用户信息(jsonToken)给微服务,这样下游微服务就不需要关心令牌格式解析以及OAuth2.0相关机制了
API网关在认证授权体系里主要负责两件事:

  1. 作为OAuth2.0的资源服务器角色,实现接入方权限拦截
  2. 令牌解析并转发当前登录用户信息(明文token)给微服务,微服务拿到明文token(明文token中包含登录用户的身份求和权限信息)后也需要做两件事:
    1. 用户授权拦截(看当前用户是否有权访问该资源)
    2. 将用户信息存储进当前线程上下文(有利于后续业务逻辑随时获取当前用户信息)
      创建新maven
      依赖如下
<dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>com.netflix.hystrix</groupId>
            <artifactId>hystrix-javanica</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.retry</groupId>
            <artifactId>spring-retry</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-jwt</artifactId>
        </dependency>
        <dependency>
            <groupId>javax.interceptor</groupId>
            <artifactId>javax.interceptor-api</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>
@Configuration
@EnableResourceServer
public class ResourceServerConfig{
    public static final String RESOURCE_ID = "res1";
    // uaa资源
    @Configuration
    @EnableResourceServer
    public class UAAServerConfig extends ResourceServerConfigurerAdapter{
        @Autowired
        private TokenStore tokenStore;
        @Override
        public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
            resources.resourceId(RESOURCE_ID)
                    .tokenStore(tokenStore)
                    //.tokenServices(tokenService()) //验证令牌的服务
                    .stateless(true);
        }

        @Override
        public void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                    .antMatchers("/uaa/**").permitAll();
        }
    }
    // order资源
    @Configuration
    @EnableResourceServer
    public class OrderServerConfig extends ResourceServerConfigurerAdapter{
        @Autowired
        private TokenStore tokenStore;
        @Override
        public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
            resources.resourceId(RESOURCE_ID)
                    .tokenStore(tokenStore)
                    //.tokenServices(tokenService()) //验证令牌的服务
                    .stateless(true);
        }

        @Override
        public void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                    .antMatchers("/order/**").access("#oauth2.hasScope('ROLE_API')");
        }
    }
    //配置其他的资源服务
}
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    // 安全拦截机制

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/**").permitAll()
                .and().csrf().disable();
    }
}

转发明文token给微服务

通过Zuul过滤器的方式实现,目的是让下游微服务能够很方便的获取到当前的登录用户信息(明文token)

  1. 实现Zuul前置过滤器,完成当前登录用户信息提取,并放入转发微服务的request中
public class AuthFilter extends ZuulFilter {

    @Override
    public String filterType() {
        return "pre";
    }

    @Override
    public int filterOrder() {
        return 0;
    }

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

    @Override
    public Object run() throws ZuulException {
        RequestContext ctx = RequestContext.getCurrentContext();
        // 从安全上下文中拿到用户身份对象
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if(!(authentication instanceof OAuth2Authentication)){
            return null;
        }
        OAuth2Authentication oAuth2Authentication = (OAuth2Authentication) authentication;
        Authentication userAuthentication = oAuth2Authentication.getUserAuthentication();
        // 取出用户身份
        String principal = userAuthentication.getName();
        // 获取当前用户权限信息
        List<String> authorities = new ArrayList<>();
        userAuthentication.getAuthorities().stream().forEach(c -> authorities.add(((GrantedAuthority) c ).getAuthority()));

        OAuth2Request oAuth2Request = oAuth2Authentication.getOAuth2Request();
        Map<String, String> requestParameters = oAuth2Request.getRequestParameters();
        Map<String,Object> jsonToken = new HashMap<>(requestParameters);
        if(userAuthentication != null){
            jsonToken.put("principal", principal);
            jsonToken.put("authorities", authorities);
        }
        // 把身份信息和权限信息放在json中,加入http的header中
        ctx.addZuulRequestHeader("json-token", JSON.toJSONString(jsonToken));
        // 转发给微服务
        return null;
    }
@Configuration
public class ZuulConfig {
    @Bean
    public AuthFilter preFilter(){
        return new AuthFilter();
    }
    @Bean
    public FilterRegistrationBean corsFilter(){
        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        final CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.addAllowedOrigin("*");
        config.addAllowedHeader("*");
        config.addAllowedMethod("*");
        config.setMaxAge(18000L);
        source.registerCorsConfiguration("/**", config);
        CorsFilter corsFilter = new CorsFilter(source);
        FilterRegistrationBean bean = new FilterRegistrationBean(corsFilter);
        bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
        return bean;
    }
}

微服务用户鉴权拦截

当微服务收到明文token时,应该怎么鉴权拦截呢?自己实现一个filter?自己解析明文token,自己定义一套资源访问策略?
能不能适配SpringSecurity呢

增加测试资源

OrderController增加一下endpoint

@Component
public class TokenAuthenticationFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        // 解析出头中的token
        String token = httpServletRequest.getHeader("json-token");
        if(token != null){
            // 将token转化为诶json对象
            JSONObject jsonObject = JSON.parseObject(token);
            // 用户身份信息
            String principal = jsonObject.getString("principal");
            // 用户权限
            JSONArray authoritiesArray = jsonObject.getJSONArray("authorities");
            String[] authorities = authoritiesArray.toArray(new String[authoritiesArray.size()]);

            // 将身份信息和权限填充到用户token对象中
            UsernamePasswordAuthenticationToken authenticationToken
                    = new UsernamePasswordAuthenticationToken(principal, null, AuthorityUtils.createAuthorityList(authorities));
            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
            // 将authrnticationToken填充到安全上下文
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        }
        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }
}
;