SpringSecurity的使用以及集成JWT、OAuth2.0实现单点登录

1、SpringSecurity 框架简介

1.1、概要

Spring 是非常流行和成功的 Java 应用开发框架,Spring Security 正是 Spring 家族中的成员。Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案,一般来说,Web 应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分,这两点也是 Spring Security 重要核心功能。

  • 用户认证指的是:验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。通俗点说就是系统认为用户是否能登录。
  • 用户授权指的是:验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。通俗点讲就是系统判断用户是否有权限去做某些事情。通俗点讲就是系统判断用户是否有权限去做某些事情。

1.2、SpringSecurity和Shiro的区别

1.2.1、SpringSecurity

Spring 技术栈的组成部分,通过提供完整可扩展的认证和授权支持保护你的应用程序。
SpringSecurity 特点:

  • 和 Spring 无缝整合。
  • 全面的权限控制。
  • 专门为 Web 开发而设计
  • 重量级

1.2.2、Shiro

Apache 旗下的轻量级权限控制框架。
Shiro特点:

  • 轻量级。Shiro 主张的理念是把复杂的事情变简单。针对对性能有更高要求的互联网应用有更好表现。
  • 通用性。
    1. 好处:不局限于 Web 环境,可以脱离 Web 环境使用。
    2. 缺陷:在 Web 环境下一些特定的需求需要手动编写代码定制。

因此,一般来说,常见的安全管理技术栈的组合是这样的:
• SSM + Shiro
• Spring Boot/Spring Cloud + Spring Security
以上只是一个推荐的组合而已,如果单纯从技术上来说,无论怎么组合,都是可以运行的。

1.3、权限管理中的相关概念

1.3.1 主体

英文单词:principal,使用系统的用户或设备或从其他系统远程登录的用户等等。简单说就是谁使用系统谁就是主体。

1.3.2 认证

英文单词:authentication,权限管理系统确认一个主体的身份,允许主体进入系统。简单说就是“主体”证明自己是谁。笼统的认为就是以前所做的登录操作。

1.3.3 授权

英文单词:authorization,将操作系统的“权力”“授予”“主体”,这样主体就具备了操作系统中特定功能的能力。所以简单来说,授权就是给用户分配权限。

1.4、SpringSecurity 基本原理

SpringSecurity 本质是一个过滤器链:从启动时可以获取到过滤器链:

org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter
org.springframework.security.web.context.SecurityContextPersistenceFilter 
org.springframework.security.web.header.HeaderWriterFilter
org.springframework.security.web.csrf.CsrfFilter
org.springframework.security.web.authentication.logout.LogoutFilter 
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter 
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter 
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter
org.springframework.security.web.savedrequest.RequestCacheAwareFilter
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
org.springframework.security.web.authentication.AnonymousAuthenticationFilter 
org.springframework.security.web.session.SessionManagementFilter 
org.springframework.security.web.access.ExceptionTranslationFilter 
org.springframework.security.web.access.intercept.FilterSecurityInterceptor

1.4.1、代码底层流程:重点看三个过滤器:

  1. FilterSecurityInterceptor:是一个方法级的权限过滤器, 基本位于过滤链的最底部。
    SpringSecurity的使用以及集成JWT、OAuth2.0实现单点登录_第1张图片
    super.beforeInvocation(fi) 表示查看之前的 filter 是否通过。
    fi.getChain().doFilter(fi.getRequest(), fi.getResponse());表示真正的调用后台的服务。

  2. ExceptionTranslationFilter:是个异常过滤器,用来处理在认证授权过程中抛出的异常
    SpringSecurity的使用以及集成JWT、OAuth2.0实现单点登录_第2张图片

  3. UsernamePasswordAuthenticationFilter :对/login 的 POST 请求做拦截,校验表单中用户名,密码。
    SpringSecurity的使用以及集成JWT、OAuth2.0实现单点登录_第3张图片

1.4.2、UserDetailsService 接口

当什么也没有配置的时候,账号和密码是由 Spring Security 定义生成的。而在实际项目中账号和密码都是从数据库中查询出来的。 所以我们要通过自定义逻辑控制认证逻辑。如果需要自定义逻辑时,只需要实现 UserDetailsService 接口即可。接口定义如下:
SpringSecurity的使用以及集成JWT、OAuth2.0实现单点登录_第4张图片
返回值为UserDetails,这个类是系统默认的用户“主体”

// 表示获取登录用户所有权限
Collection<? extends GrantedAuthority> getAuthorities();
// 表示获取密码
String getPassword();
// 表示获取用户名
String getUsername();
// 表示判断账户是否过期
boolean isAccountNonExpired();
// 表示判断账户是否被锁定
boolean isAccountNonLocked();
// 表示凭证{密码}是否过期
boolean isCredentialsNonExpired();
// 表示当前用户是否可用
boolean isEnabled();

SpringSecurity的使用以及集成JWT、OAuth2.0实现单点登录_第5张图片

1.4.3、PasswordEncoder 接口讲解

// 表示把参数按照特定的解析规则进行解析
String encode(CharSequence rawPassword);
// 表示验证从存储中获取的编码密码与编码后提交的原始密码是否匹配。如果密码匹配,则返回 true;如果不匹配,则返回 false。第一个参数表示需要被解析的密码。第二个参数表示存储的密码。
boolean matches(CharSequence rawPassword, String encodedPassword);
// 表示如果解析的密码能够再次进行解析且达到更安全的结果则返回 true,否则返回false。默认返回 false。
default boolean upgradeEncoding(String encodedPassword) {
return false;}

SpringSecurity的使用以及集成JWT、OAuth2.0实现单点登录_第6张图片
BCryptPasswordEncoder 是 Spring Security 官方推荐的密码解析器,平时多使用这个解析器。

BCryptPasswordEncoder 是对 bcrypt 强散列方法的具体实现。是基于 Hash 算法实现的单向加密。可以通过 strength 控制加密强度,默认 10.

BCryptPasswordEncoder的使用方式

 public static void main(String[] args) {
        // 创建密码解析器
        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
        // 对密码进行加密
        String song = bCryptPasswordEncoder.encode("song");
        // 打印加密之后的数据
        System.out.println("加密之后数据:\t" + song);
        //判断原字符加密后和加密之前是否匹配
        boolean result = bCryptPasswordEncoder.matches("song", song);
        // 打印比较结果
        System.out.println("比较结果:\t" + result);
    }

1.5、SpringSecurity Web 权限方案

1.5.1、设置登录系统的账号、密码

1.5.1.1、方式一:在 application.properties 配置对应的账号、密码

pom

<dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-securityartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <optional>trueoptional>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
        dependency>
    dependencies>

yml

server:
  port: 8888

spring:
  application:
    name: security
  security:
    user:
      name: song
      password: song

controller

@RestController
@RequestMapping("/hello")
public class HelloController {

    @GetMapping("/sayHello")
    public String sayHello(){
        System.out.println("sayHello ....................   ");
        return "ok";
    }
}

测试

请求接口 http://localhost:8888/hello/sayHello ,输入账号,密码,认证成功,返回接口信息

SpringSecurity的使用以及集成JWT、OAuth2.0实现单点登录_第7张图片
SpringSecurity的使用以及集成JWT、OAuth2.0实现单点登录_第8张图片

1.5.1.2、方式二:增加配置类,通过配置类配置用户名和密码
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
        String password = bCryptPasswordEncoder.encode("lisi");
        auth.inMemoryAuthentication().withUser("lisi").password(password).roles("admin");
    }

    // 注入 PasswordEncoder 类到 spring 容器中
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

测试
SpringSecurity的使用以及集成JWT、OAuth2.0实现单点登录_第9张图片
SpringSecurity的使用以及集成JWT、OAuth2.0实现单点登录_第10张图片

1.5.1.3、方式三:自定义实现, 下面是实现方案,可用于生产开发

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

基于RBAC模型创建用户、资源、角色数据库表
SpringSecurity的使用以及集成JWT、OAuth2.0实现单点登录_第12张图片

1.5.1.3.1、编写核心配置类

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

package com.song.serurity.config;

import com.song.serurity.filter.TokenAuthenticationFilter;
import com.song.serurity.filter.TokenLoginFilter;
import com.song.serurity.security.DefaultPasswordEncoder;
import com.song.serurity.security.TokenLogoutHandler;
import com.song.serurity.security.TokenManager;
import com.song.serurity.security.UnauthorizedEntryPoint;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;

import javax.annotation.Resource;

/**
 * 

* Security配置类 *

*/
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class TokenWebSecurityConfig extends WebSecurityConfigurerAdapter { private UserDetailsService userDetailsService; private TokenManager tokenManager; private DefaultPasswordEncoder defaultPasswordEncoder; private RedisTemplate redisTemplate; @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 */ @Override protected void configure(HttpSecurity http) throws Exception { http.exceptionHandling() .authenticationEntryPoint(new UnauthorizedEntryPoint()) .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 TokenAuthenticationFilter(authenticationManager(), tokenManager, redisTemplate)).httpBasic(); } /** * 密码处理 * @param auth * @throws Exception */ @Override public void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(defaultPasswordEncoder); } /** * 配置哪些请求不拦截 * @param web * @throws Exception */ @Override public void configure(WebSecurity web) throws Exception { // web.ignoring().antMatchers("/api/**", // "/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**" // ); web.ignoring().antMatchers("/*/**" ); } }
1.5.1.3.2、核心配置类的相关工具类

TokenManager

package com.song.serurity.security;

import io.jsonwebtoken.CompressionCodecs;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.stereotype.Component;

import java.util.Date;

/**
 * 

* token管理 *

*/
@Component public class TokenManager { private long tokenExpiration = 24*60*60*1000; private String tokenSignKey = "123456"; public String createToken(String username) { String token = Jwts.builder().setSubject(username) .setExpiration(new Date(System.currentTimeMillis() + tokenExpiration)) .signWith(SignatureAlgorithm.HS512, tokenSignKey).compressWith(CompressionCodecs.GZIP).compact(); return token; } public String getUserFromToken(String token) { String user = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token).getBody().getSubject(); return user; } public void removeToken(String token) { //jwttoken无需删除,客户端扔掉即可。 } }

DefaultPasswordEncoder

package com.song.serurity.security;

import com.song.commonutils.Md5Utils;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

/**
 * 

* t密码的处理方法类型 *

*/
@Component public class DefaultPasswordEncoder implements PasswordEncoder { public DefaultPasswordEncoder() { this(-1); } /** * @param strength the log rounds to use, between 4 and 31 */ public DefaultPasswordEncoder(int strength) { } public String encode(CharSequence rawPassword) { return Md5Utils.md5(rawPassword.toString(), Md5Utils.salt, 1); } public boolean matches(CharSequence rawPassword, String encodedPassword) { return encodedPassword.equals(Md5Utils.md5(rawPassword.toString(), Md5Utils.salt, 1)); } }

Md5Utils

package com.song.commonutils;


import com.baomidou.mybatisplus.core.toolkit.StringUtils;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

/**
 * @description: Md5工具类
 **/
public class Md5Utils {


    public static final String salt = "ACCOUNT";

    /**
     * md5加密
     *
     * @param source 需要加密的字段
     * @param salt   盐
     * @param num    散列次数
     * @return 加密后的值
     */
    public static String md5(String source, String salt, int num) {
        if (StringUtils.isNotBlank(salt)) {
            source += salt;
        }
        StringBuilder builder = new StringBuilder();
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            md.update(source.getBytes());
            byte[] b = md.digest();
            for (int value : b) {
                int i = value;
                if (i < 0) {
                    i += 256;
                }
                if (i < 16) {
                    builder.append("0");
                }
                builder.append(Integer.toHexString(i));
            }
            if (num > 0) {
                source = md5(builder.toString(), salt, --num);
            }
        } catch (NoSuchAlgorithmException exception) {
            exception.printStackTrace();
        }

        return source.toUpperCase();
    }

    public static String md5(String source) throws NoSuchAlgorithmException {
        return md5(source, null, 1);
    }
}

TokenLogoutHandler

package com.song.serurity.security;

import com.song.commonutils.R;
import com.song.commonutils.ResponseUtil;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutHandler;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 

* 登出业务逻辑类 *

*/
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) { String token = request.getHeader("token"); if (token != null) { tokenManager.removeToken(token); //清空当前用户缓存中的权限数据 String userName = tokenManager.getUserFromToken(token); redisTemplate.delete(userName); } ResponseUtil.out(response, R.success()); } }

UnauthorizedEntryPoint

package com.song.serurity.security;


import com.song.commonutils.R;
import com.song.commonutils.ResponseUtil;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 

* 未授权的统一处理方式 *

*/
public class UnauthorizedEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { ResponseUtil.out(response, R.error()); } }
1.5.1.3.3、创建认证授权实体类
package com.song.serurity.entity;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.util.StringUtils;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/**
 * 

* 安全认证用户详情信息 *

*/
@Data @Slf4j 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

package com.song.serurity.entity;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import java.io.Serializable;

/**
 * 

* 用户实体类 *

*/
@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; }
1.5.1.3.4、创建认证的 filter
package com.song.serurity.filter;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.song.commonutils.R;
import com.song.commonutils.ResponseUtil;
import com.song.serurity.entity.SecurityUser;
import com.song.serurity.entity.User;
import com.song.serurity.security.TokenManager;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;

/**
 * 

* 登录过滤器,继承UsernamePasswordAuthenticationFilter,对用户名密码进行登录校验 *

*/
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter { private AuthenticationManager authenticationManager; private TokenManager tokenManager; private RedisTemplate redisTemplate; 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")); } @Override public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res) throws AuthenticationException { try { User user = new ObjectMapper().readValue(req.getInputStream(), User.class); return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), new ArrayList<>())); } catch (IOException e) { throw new RuntimeException(e); } } /** * 登录成功 * * @param req * @param res * @param chain * @param auth * @throws IOException * @throws ServletException */ @Override protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain, Authentication auth) throws IOException, ServletException { SecurityUser user = (SecurityUser) auth.getPrincipal(); String token = tokenManager.createToken(user.getCurrentUserInfo().getUsername()); redisTemplate.opsForValue().set(user.getCurrentUserInfo().getUsername(), user.getPermissionValueList()); ResponseUtil.out(res, R.success().data("token", token)); } /** * 登录失败 * * @param request * @param response * @param e * @throws IOException * @throws ServletException */ @Override protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException { ResponseUtil.out(response, R.error()); } }
1.5.1.3.5、创建授权的 filter
package com.song.serurity.filter;

import com.song.commonutils.R;
import com.song.commonutils.ResponseUtil;
import com.song.serurity.security.TokenManager;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.util.StringUtils;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/**
 * 

* 访问过滤器 *

*/
public class TokenAuthenticationFilter extends BasicAuthenticationFilter { private TokenManager tokenManager; private RedisTemplate redisTemplate; public TokenAuthenticationFilter(AuthenticationManager authManager, TokenManager tokenManager,RedisTemplate redisTemplate) { super(authManager); this.tokenManager = tokenManager; this.redisTemplate = redisTemplate; } @Override protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException { logger.info("================="+req.getRequestURI()); if(req.getRequestURI().indexOf("admin") == -1) { chain.doFilter(req, res); return; } UsernamePasswordAuthenticationToken authentication = null; try { authentication = getAuthentication(req); } catch (Exception e) { ResponseUtil.out(res, R.error()); } if (authentication != null) { SecurityContextHolder.getContext().setAuthentication(authentication); } else { ResponseUtil.out(res, R.error()); } chain.doFilter(req, res); } private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) { // token置于header里 String token = request.getHeader("token"); if (token != null && !"".equals(token.trim())) { String userName = tokenManager.getUserFromToken(token); List<String> permissionValueList = (List<String>) redisTemplate.opsForValue().get(userName); Collection<GrantedAuthority> authorities = new ArrayList<>(); for(String permissionValue : permissionValueList) { if(StringUtils.isEmpty(permissionValue)) continue; SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permissionValue); authorities.add(authority); } if (!StringUtils.isEmpty(userName)) { return new UsernamePasswordAuthenticationToken(userName, token, authorities); } return null; } return null; } }

1.6、SpringSecurity 原理总结

1.6.1、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, 用户将不必填写用户名和密码, 而是直接登录进入系统,该过滤器默认不开启。

1.6.2、SpringSecurity 基本流程

Spring Security 采取过滤链实现认证与授权,只有当前过滤器通过,才能进入下一个过滤器:
SpringSecurity的使用以及集成JWT、OAuth2.0实现单点登录_第13张图片
绿色部分是认证过滤器,需要我们自己配置,可以配置多个认证过滤器。认证过滤器可以使用 Spring Security 提供的认证过滤器,也可以自定义过滤器(例如:短信验证)。认证过滤器要在 configure(HttpSecurity http)方法中配置,没有配置不生效。下面会重点介绍以下三个过滤器:

  • UsernamePasswordAuthenticationFilter过滤器 :该过滤器会拦截前端提交的 POST 方式的登录表单请求,并进行身份认证。
  • ExceptionTranslationFilter 过滤器:该过滤器不需要我们配置,对于前端提交的请求会直接放行,捕获后续抛出的异常并进行处理(例如:权限访问限制)。
  • FilterSecurityInterceptor 过滤器:该过滤器是过滤器链的最后一个过滤器,根据资源权限配置来判断当前请求是否有权限访问对应的资源。如果访问受限会抛出相关异常,并由 ExceptionTranslationFilter 过滤器进行捕获和处理。

1.6.3、SpringSecurity 认证流程

认证流程是在 UsernamePasswordAuthenticationFilter 过滤器中处理的,具体流程如下所示:
SpringSecurity的使用以及集成JWT、OAuth2.0实现单点登录_第14张图片

SpringSecurity的使用以及集成JWT、OAuth2.0实现单点登录_第15张图片
让我们仔细分析认证过程:

  1. 用户提交用户名、密码被SecurityFilterChain中的 UsernamePasswordAuthenticationFilter 过滤器获取到, 封装为请求Authentication,通常情况下是UsernamePasswordAuthenticationToken这个实现类。
  2. 然后过滤器将Authentication提交至认证管理器(AuthenticationManager)进行认证
  3. 认证成功后, AuthenticationManager 身份管理器返回一个被填充满了信息的(包括上面提到的权限信息, 身份信息,细节信息,但密码通常会被移除) Authentication 实例。
  4. SecurityContextHolder 安全上下文容器将第3步填充了信息的 Authentication ,通过 SecurityContextHolder.getContext().setAuthentication(…)方法,设置到其中。 可以看出AuthenticationManager接口(认证管理器)是认证相关的核心接口,也是发起认证的出发点,它 的实现类为ProviderManager。而Spring Security支持多种认证方式,因此ProviderManager维护着一个 List 列表,存放多种认证方式,最终实际的认证工作是由 AuthenticationProvider完成的。咱们知道web表单的对应的AuthenticationProvider实现类为 DaoAuthenticationProvider,它的内部又维护着一个UserDetailsService负责UserDetails的获取。最终 AuthenticationProvider将UserDetails填充至Authentication。

1.6.4、SpringSecurity 授权流程

Spring Security的授权流程如下:
SpringSecurity的使用以及集成JWT、OAuth2.0实现单点登录_第16张图片
分析授权流程:

  1. 拦截请求,已认证用户访问受保护的web资源将被SecurityFilterChain中的 FilterSecurityInterceptor 的子 类拦截。
  2. 获取资源访问策略,FilterSecurityInterceptor会从 SecurityMetadataSource 的子类 DefaultFilterInvocationSecurityMetadataSource 获取要访问当前资源所需要的权限 Collection 。 SecurityMetadataSource其实就是读取访问策略的抽象,而读取的内容,其实就是我们配置的访问规则, 读 取访问策略如:
http.authorizeRequests() .antMatchers("/r/r1").hasAuthority("p1") .antMatchers("/r/r2").hasAuthority("p2") ...
  1. 最后,FilterSecurityInterceptor会调用 AccessDecisionManager 进行授权决策,若决策通过,则允许访问资 源,否则将禁止访问。
    AccessDecisionManager(访问决策管理器)的核心接口如下:
public interface AccessDecisionManager { 
	/** * 通过传递的参数来决定用户是否有访问对应受保护资源的权限 */
	void decide(Authentication authentication , Object object, Collection<ConfigAttribute> configAttributes ) throws AccessDeniedException, InsufficientAuthenticationException; 
	//略.. 
}

这里着重说明一下decide的参数:

  • authentication:要访问资源的访问者的身份
  • object:要访问的受保护资源,web请求对应FilterInvocation
  • configAttributes:是受保护资源的访问策略,通过SecurityMetadataSource获取。
    decide接口就是用来鉴定当前用户是否有访问对应受保护资源的权限。

1.6.5、SpringSecurity 退出流程

点击退出方法,设置退出时都调用的退出接口,成功退出后,也可以设置自定义退出成功的页面:
在WebSecurityConfig的protected void configure(HttpSecurity http)中配置:

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

但是如果在退出的时候需要处理一些退出操作,比方说使

  • HTTP Session 无效
  • 清除 SecurityContextHolder
  • 跳转到 /login-view?logout
    可以自定义退出功能,
@Override protected void configure(HttpSecurity http) throws Exception { 
http .authorizeRequests() //... 
	.and() 
	.logout()
	.logoutUrl("/logout")
	.logoutSuccessUrl("/login‐view?logout")
	.logoutSuccessHandler(logoutSuccessHandler)
	.addLogoutHandler(logoutHandler)
	.invalidateHttpSession(true); 
  }
  1. 提供系统退出支持,使用 WebSecurityConfigurerAdapter 会自动被应用
  2. 设置触发退出操作的URL (默认是 /logout ).
  3. 退出之后跳转的URL。默认是 /login?logout 。
  4. 定制的 LogoutSuccessHandler ,用于实现用户退出成功时的处理。如果指定了这个选项那么 logoutSuccessUrl() 的设置会被忽略。
  5. 添加一个 LogoutHandler ,用于实现用户退出时的清理工作.默认 SecurityContextLogoutHandler 会被添加 为最后一个 LogoutHandler 。
  6. 指定是否在退出时让 HttpSession 无效。 默认设置为 true。

注意:如果让logout在GET请求下生效,必须关闭防止CSRF攻击csrf().disable()。如果开启了CSRF,必须使用 post方式请求/logout

logoutHandler:
一般来说, LogoutHandler 的实现类被用来执行必要的清理,因而他们不应该抛出异常。
下面是Spring Security提供的一些实现:

  • PersistentTokenBasedRememberMeServices 基于持久化token的RememberMe功能的相关清理
  • TokenBasedRememberMeService 基于token的RememberMe功能的相关清理
  • CookieClearingLogoutHandler 退出时Cookie的相关清理
  • CsrfLogoutHandler 负责在退出时移除csrfToken
  • SecurityContextLogoutHandler 退出时SecurityContext的相关清理

链式API提供了调用相应的 LogoutHandler 实现的快捷方式,比如deleteCookies()。

1.7、分布式系统认证方案

1.7.1、基于session的认证方式

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

SpringSecurity的使用以及集成JWT、OAuth2.0实现单点登录_第17张图片
这个时候,通常的做法有下面几种:

  • Session复制:多台应用服务器之间同步session,使session保持一致,对外透明。
  • Session黏贴:当用户访问集群中某台服务器后,强制指定后续所有请求均落到此机器上。
  • Session集中存储:将Session存入分布式缓存中,所有服务器应用实例统一从分布式缓存中存取Session。

总体来讲,基于session认证的认证方式,可以更好的在服务端对会话进行控制,且安全性较高。但是,session机 制方式基于cookie,在复杂多样的移动客户端上不能有效的使用,并且无法跨域,另外随着系统的扩展需提高 session的复制、黏贴及存储的容错性。

1.7.2、基于token的认证方式

基于token的认证方式,服务端不用存储认证数据,易维护扩展性强, 客户端可以把token 存在任意地方,并且可以实现web和app统一认证机制。其缺点也很明显,token由于自包含信息,因此一般数据量较大,而且每次请求都需要传递,因此比较占带宽。另外,token的签名验签操作也会给cpu带来额外的处理负担。
SpringSecurity的使用以及集成JWT、OAuth2.0实现单点登录_第18张图片

1.7.3、一般分布式系统中选择的技术方案

一般采用采用基于token的认证方式,它的优点是:

  1. 适合统一认证的机制,客户端、一方应用、三方应用都遵循一致的认证机制。
  2. token认证方式对第三方应用接入更适合,因为它更开放,可使用当前有流行的开放协议Oauth2.0、JWT等。
  3. 一般情况服务端无需存储会话信息,减轻了服务端的压力。

SpringSecurity的使用以及集成JWT、OAuth2.0实现单点登录_第19张图片
流程描述:

  • (1)用户通过接入方(应用)登录,接入方采取OAuth2.0方式在统一认证服务(UAA)中认证。
  • (2)认证服务(UAA)调用验证该用户的身份是否合法,并获取用户权限信息。
  • (3)认证服务(UAA)获取接入方权限信息,并验证接入方是否合法。
  • (4)若登录用户以及接入方都合法,认证服务生成jwt令牌返回给接入方,其中jwt中包含了用户权限及接入方权 限。
  • (5)后续,接入方携带jwt令牌对API网关内的微服务资源进行访问。
  • (6)API网关对令牌解析、并验证接入方的权限是否能够访问本次请求的微服务。
  • (7)如果接入方的权限没问题,API网关将原请求header中附加解析后的明文Token,并将请求转发至微服务。
  • (8)微服务收到请求,明文token中包含登录用户的身份和权限信息。因此后续微服务自己可以干两件事:
    • 用 户授权拦截(看当前用户是否有权访问该资源)
    • 将用户信息存储进当前线程上下文(有利于后续业务逻辑随时 获取当前用户信息)

流程所涉及到UAA服务、API网关这三个组件职责如下:

  • 1)统一认证服务(UAA) 它承载了OAuth2.0接入方认证、登入用户的认证、授权以及生成令牌的职责,完成实际的用户认证、授权功能。
  • 2)API网关 作为系统的唯一入口,API网关为接入方提供定制的API集合,它可能还具有其它职责,如身份验证、监控、负载均 衡、缓存等。API网关方式的核心要点是,所有的接入方和消费端都通过统一的网关接入微服务,在网关层处理所 有的非业务功能。

1.8、OAuth2.0

1.8.1、OAuth2.0介绍

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

SpringSecurity的使用以及集成JWT、OAuth2.0实现单点登录_第20张图片
OAauth2.0包括以下角色:

  1. 客户端:本身不存储资源,需要通过资源拥有者的授权去请求资源服务器的资源,比如:Android客户端、Web客户端(浏 览器端)、微信客户端等。
  2. 资源拥有者: 通常为用户,也可以是应用程序,即该资源的拥有者。
  3. 授权服务器(也称认证服务器):用于服务提供商对资源拥有的身份进行认证、对访问资源进行授权,认证成功后会给客户端发放令牌 (access_token),作为客户端访问资源服务器的凭据。
  4. 资源服务器:存储资源的服务器,本例子为微信存储的用户信息。

现在还有一个问题,服务提供商能允许随便一个客户端就接入到它的授权服务器吗?答案是否定的,服务提供商会 给准入的接入方一个身份,用于接入时的凭据:

  • client_id:客户端标识
  • client_secret:客户端秘钥

因此,准确来说,授权服务器对两种OAuth2.0中的两个角色进行认证授权,分别是资源拥有者、客户端。

1.8.2、授权服务器配置

Spring-Security-OAuth2是对OAuth2的一种实现,并且跟我们之前学习的Spring Security相辅相成,与Spring Cloud体系的集成也非常便利,最终使用它来实现我们设计的分布式认证授权解 决方案。
OAuth2.0的服务提供方涵盖两个服务,即授权服务 (Authorization Server,也叫认证服务) 和资源服务 (Resource Server),使用 Spring Security OAuth2 的时候你可以选择把它们在同一个应用程序中实现,也可以选择建立使用 同一个授权服务的多个资源服务。

授权服务 (Authorization Server)应包含对接入端以及登入用户的合法性进行验证并颁发token等功能,对令牌 的请求端点由 Spring MVC 控制器进行实现,下面是配置一个认证服务必须要实现的endpoints:

  • AuthorizationEndpoint 服务于认证请求。默认 URL: /oauth/authorize 。
  • TokenEndpoint 服务于访问令牌的请求。默认 URL: /oauth/token 。
    资源服务 (Resource Server),应包含对资源的保护功能,对非法请求进行拦截,对请求中token进行解析鉴 权等,下面的过滤器用于实现 OAuth 2.0 资源服务: OAuth2AuthenticationProcessingFilter用来对请求给出的身份令牌解析鉴权。

SpringSecurity的使用以及集成JWT、OAuth2.0实现单点登录_第21张图片
认证流程如下:

  1. 客户端请求UAA授权服务进行认证。
  2. 认证通过后由UAA颁发令牌。
  3. 客户端携带令牌Token请求资源服务。
  4. 资源服务校验令牌的合法性,合法即返回资源信息。
1.8.2.1、EnableAuthorizationServer

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

@Configuration 
@EnableAuthorizationServer 
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter { 
	//略...
 }

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

public class AuthorizationServerConfigurerAdapter implements AuthorizationServerConfigurer { 
	public AuthorizationServerConfigurerAdapter() {} 
	public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {} 
	public void configure(ClientDetailsServiceConfigurer clients) throws Exception {} 
	public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {} 
}
  • ClientDetailsServiceConfigurer:用来配置客户端详情服务(ClientDetailsService),客户端详情信息在 这里进行初始化,你能够把客户端详情信息写死在这里或者是通过数据库来存储调取详情信息。
  • AuthorizationServerEndpointsConfigurer:用来配置令牌(token)的访问端点和令牌服务(token services)。
  • AuthorizationServerSecurityConfigurer:用来配置令牌端点的安全约束.
1.8.2.1.1、配置客户端详细信息

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

  • clientId:(必须的)用来标识客户的Id。
  • secret:(需要值得信任的客户端)客户端安全码,如果有的话。
  • scope:用来限制客户端的访问范围,如果为空(默认)的话,那么客户端拥有全部的访问范围。
  • authorizedGrantTypes:此客户端可以使用的授权类型,默认为空。
  • authorities:此客户端可以使用的权限(基于Spring Security authorities)。

客户端详情(Client Details)能够在应用程序运行的时候进行更新,可以通过访问底层的存储服务(例如将客户 端详情存储在一个关系数据库的表中,就可以使用 JdbcClientDetailsService)或者通过自己实现 ClientRegistrationService接口(同时你也可以实现 ClientDetailsService 接口)来进行管理。

1.8.2.1.2、管理令牌

AuthorizationServerTokenServices 接口定义了一些操作使得你可以对令牌进行一些必要的管理,令牌可以被用来 加载身份信息,里面包含了这个令牌的相关权限。

自己可以创建 AuthorizationServerTokenServices 这个接口的实现,则需要继承 DefaultTokenServices 这个类, 里面包含了一些有用实现,你可以使用它来修改令牌的格式和令牌的存储。默认的,当它尝试创建一个令牌的时 候,是使用随机值来进行填充的,除了持久化令牌是委托一个 TokenStore 接口来实现以外,这个类几乎帮你做了 所有的事情。并且 TokenStore 这个接口有一个默认的实现,它就是 InMemoryTokenStore ,如其命名,所有的 令牌是被保存在了内存中。除了使用这个类以外,你还可以使用一些其他的预定义实现,下面有几个版本,它们都 实现了TokenStore接口:

  • InMemoryTokenStore:这个版本的实现是被默认采用的,它可以完美的工作在单服务器上(即访问并发量 压力不大的情况下,并且它在失败的时候不会进行备份),大多数的项目都可以使用这个版本的实现来进行 尝试,你可以在开发的时候使用它来进行管理,因为不会被保存到磁盘中,所以更易于调试。
  • JdbcTokenStore:这是一个基于JDBC的实现版本,令牌会被保存进关系型数据库。使用这个版本的实现时, 你可以在不同的服务器之间共享令牌信息,使用这个版本的时候请注意把"spring-jdbc"这个依赖加入到你的 classpath当中。
  • JwtTokenStore:这个版本的全称是 JSON Web Token(JWT),它可以把令牌相关的数据进行编码(因此对 于后端服务来说,它不需要进行存储,这将是一个重大优势),但是它有一个缺点,那就是撤销一个已经授 权令牌将会非常困难,所以它通常用来处理一个生命周期较短的令牌以及撤销刷新令牌(refresh_token)。 另外一个缺点就是这个令牌占用的空间会比较大,如果你加入了比较多用户凭证信息。JwtTokenStore 不会保存任何数据,但是它在转换令牌值以及授权信息方面与 DefaultTokenServices 所扮演的角色是一样的。
  1. 定义TokenConfig,在config包下定义TokenConfig,生成一个令牌。
  2. 定义AuthorizationServerTokenServices 在AuthorizationServer中定义AuthorizationServerTokenServices
1.8.2.1.3、令牌访问端点配置

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

配置授权类型(Grant Types)

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

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

配置授权端点的URL(Endpoint URLs):
AuthorizationServerEndpointsConfigurer 这个配置对象有一个叫做 pathMapping() 的方法用来配置端点URL链 接,它有两个参数:

  • 第一个参数:String 类型的,这个端点URL的默认链接。
  • 第二个参数:String 类型的,你要进行替代的URL链接。

以上的参数都将以 “/” 字符为开始的字符串,框架的默认URL链接如下列表,可以作为这个 pathMapping() 方法的 第一个参数:

  • /oauth/authorize:授权端点。
  • /oauth/token:令牌端点。
  • /oauth/confirm_access:用户确认授权提交端点。
  • /oauth/error:授权服务错误信息端点。
  • /oauth/check_token:用于资源服务访问的令牌解析端点。
  • /oauth/token_key:提供公有密匙的端点,如果你使用JWT令牌的话。 需要注意的是授权端点这个URL应该被Spring Security保护起来只供授权用户访问
1.8.2.1.4、令牌端点的安全约束

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

@Override 
public void configure(AuthorizationServerSecurityConfigurer security){ 
	security.tokenKeyAccess("permitAll()") 
	.checkTokenAccess("permitAll()")
	.allowFormAuthenticationForClients() ; 
}
  • (1)tokenkey这个endpoint当使用JwtToken且使用非对称加密时,资源服务用于获取公钥而开放的,这里指这个 endpoint完全公开。
  • (2)checkToken这个endpoint完全公开
  • (3) 允许表单认证

授权服务配置总结:授权服务配置分成三大块,它首先得知道客户端信息从哪儿读取,因此要进行客户端详情配置。 既然要颁发token,那必须得定义token的相关endpoint,以及token如何存取,以及客户端支持哪些类型的 token。 既然暴露除了一些endpoint,那对这些endpoint可以定义一些安全上的约束等

1.8.2.1.5、 web安全配置
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    //认证管理器
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
    //密码编码器
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    //安全拦截机制(最重要)
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/r/r1").hasAnyAuthority("p1")
                .antMatchers("/login*").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
        ;

    }
}

1.8.3、授权码模式

1.8.3.1、授权码模式

SpringSecurity的使用以及集成JWT、OAuth2.0实现单点登录_第22张图片
(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参数(授权码)。

(2)浏览器出现向授权服务器授权页面,之后将用户同意授权。

(3)授权服务器将授权码(AuthorizationCode)转经浏览器发送给client(通过redirect_uri)。

(4)客户端拿着授权码向授权服务器索要访问access_token,请求如下:/uaa/oauth/token? client_id=c1&client_secret=secret&grant_type=authorization_code&code=5PgfcD&redirect_uri=http://w ww.baidu.com

参数列表如下

  • client_id:客户端准入标识。
  • client_secret:客户端秘钥。
  • grant_type:授权类型,填写authorization_code,表示授权码模式
  • code:授权码,就是刚刚获取的授权码,注意:授权码只使用一次就无效了,需要重新申请。
  • redirect_uri:申请授权码时的跳转url,一定和申请授权码时用的redirect_uri一致。

(5)授权服务器返回令牌(access_token)
这种模式是四种模式中最安全的一种模式。一般用于client是Web服务器端应用或第三方的原生App调用资源服务 的时候。因为在这种模式中access_token不会经过浏览器或移动端的App,而是直接从服务端去交换,这样就最大 限度的减小了令牌泄漏的风险。

1.8.3.2、简化模式

SpringSecurity的使用以及集成JWT、OAuth2.0实现单点登录_第23张图片
(1)资源拥有者打开客户端,客户端要求资源拥有者给予授权,它将浏览器被重定向到授权服务器,重定向时会 附加客户端的身份信息。如:/uaa/oauth/authorize?client_id=c1&response_type=token&scope=all&redirect_uri=http://www.baidu.com
参数描述同授权码模式 ,注意response_type=token,说明是简化模式。

(2)浏览器出现向授权服务器授权页面,之后将用户同意授权。

(3)授权服务器将授权码将令牌(access_token)以Hash的形式存放在重定向uri的fargment中发送给浏览 器。

注:fragment 主要是用来标识 URI 所标识资源里的某个资源,在 URI 的末尾通过 (#)作为 fragment 的开头, 其中 # 不属于 fragment 的值。如https://domain/index#L18这个 URI 中 L18 就是 fragment 的值。大家只需要 知道js通过响应浏览器地址栏变化的方式能获取到fragment 就行了。 一般来说,简化模式用于没有服务器端的第三方单页面应用,因为没有服务器端就无法接收授权码。

1.8.3.3、密码模式

SpringSecurity的使用以及集成JWT、OAuth2.0实现单点登录_第24张图片
(1)资源拥有者将用户名、密码发送给客户端

(2)客户端拿着资源拥有者的用户名、密码向授权服务器请求令牌(access_token),请求如下:

/uaa/oauth/token? client_id=c1&client_secret=secret&grant_type=password&username=shangsan&password=123

参数列表如下:

  • client_id:客户端准入标识。
  • client_secret:客户端秘钥。
  • grant_type:授权类型,填写password表示密码模式
  • username:资源拥有者用户名。
  • password:资源拥有者密码。

(3)授权服务器将令牌(access_token)发送给client

这种模式十分简单,但是却意味着直接将用户敏感信息泄漏给了client,因此这就说明这种模式只能用于client是我 们自己开发的情况下。因此密码模式一般用于我们自己开发的,第一方原生App或第一方单页面应用。

1.8.3.4、客户端模式

SpringSecurity的使用以及集成JWT、OAuth2.0实现单点登录_第25张图片
(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本身也是安全的。因 此这种模式一般用来提供给我们完全信任的服务器端服务。比如,合作方系统对接,拉取一组用户信息。

1.8.4、资源服务器

1.8.4.1、资源服务器配置

@EnableResourceServer 注解到一个 @Configuration 配置类上,并且必须使用 ResourceServerConfigurer 这个 配置对象来进行配置(可以选择继承自 ResourceServerConfigurerAdapter 然后覆写其中的方法,参数就是这个 对象的实例),下面是一些可以配置的属性: ResourceServerSecurityConfigurer中主要包括:

  • tokenServices:ResourceServerTokenServices 类的实例,用来实现令牌服务。
  • tokenStore:TokenStore类的实例,指定令牌如何访问,与tokenServices配置可选
  • resourceId:这个资源服务的ID,这个属性是可选的,但是推荐设置并在授权服务中进行验证。
  • 其他的拓展属性例如 tokenExtractor 令牌提取器用来提取请求中的令牌。

HttpSecurity配置这个与Spring Security类似:

  • 请求匹配器,用来设置需要进行保护的资源路径,默认的情况下是保护资源服务的全部路径。
  • 通过http.authorizeRequests()来设置受保护资源的访问规则
  • 其他的自定义权限保护规则通过 HttpSecurity 来进行配置。

@EnableResourceServer 注解自动增加了一个类型为 OAuth2AuthenticationProcessingFilter 的过滤器链 编写ResouceServerConfig:

@Configuration
@EnableResourceServer
public class ResouceServerConfig extends ResourceServerConfigurerAdapter {


    public static final String RESOURCE_ID = "res1";

    @Autowired
    TokenStore tokenStore;

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

    @Override
    public void configure(HttpSecurity http) throws Exception {

        http
                .authorizeRequests()
                .antMatchers("/**").access("#oauth2.hasScope('ROLE_ADMIN')")
                .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:53020/uaa/oauth/check_token");
        service.setClientId("c1");
        service.setClientSecret("secret");
        return service;
    }*/
}
1.8.4.2、验证token

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

令牌解析方法: 使用 DefaultTokenServices 在资源服务器本地配置令牌存储、解码、解析方式 使用 RemoteTokenServices 资源服务器通过 HTTP 请求来解码令牌,每次都请求授权服务器端点 /oauth/check_token

使用授权服务的 /oauth/check_token 端点你需要在授权服务将这个端点暴露出去,以便资源服务可以进行访问, 这在咱们授权服务配置中已经提到了,下面是一个例子,在这个例子中,我们在授权服务中配置了 /oauth/check_token 和 /oauth/token_key 这两个端点:

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security){
        security
                .tokenKeyAccess("permitAll()")                    //oauth/token_key是公开
                .checkTokenAccess("permitAll()")                  //oauth/check_token公开
                .allowFormAuthenticationForClients()				//表单认证(申请令牌)
        ;
    }

在资源 服务配置RemoteTokenServices ,在ResouceServerConfig中配置

    @Bean
    public ResourceServerTokenServices tokenService() {
        //使用远程服务请求授权服务器校验token,必须指定校验token 的url、client_id,client_secret
        RemoteTokenServices service=new RemoteTokenServices();
        service.setCheckTokenEndpointUrl("http://localhost:53020/uaa/oauth/check_token");
        service.setClientId("c1");
        service.setClientSecret("secret");
        return service;
    }
        @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        resources.resourceId(RESOURCE_ID)//资源 id
                .tokenStore(tokenStore)
//                .tokenServices(tokenService())//验证令牌的服务
                .stateless(true);
    }
1.8.4.3、编写资源

    @GetMapping(value = "/r1")
    @PreAuthorize("hasAuthority('p1')")//拥有p1权限方可访问此url
    public String r1(){
        //获取用户身份信息
        UserDTO  userDTO = (UserDTO) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        return userDTO.getFullname()+"访问资源1";
    }
1.8.4.4、添加安全访问控制
@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/r1").hasAuthority("p2")
//                .antMatchers("/r/r2").hasAuthority("p2")
                .antMatchers("/r/**").authenticated()//所有/r/**的请求必须认证通过
                .anyRequest().permitAll()//除了/r/**,其它的请求可以访问
        ;


    }
}

1.9、JWT令牌

1.9.1、什么是JWT?

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

JWT令牌的优点:

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

缺点:

  • 1)JWT令牌较长,占存储空间比较大。

1.9.2、JWT令牌结构

JWT令牌由三部分组成,每部分中间使用点(.)分隔,比如:xxxxx.yyyyy.zzzzz

  • Header 头部
    包括令牌的类型(即JWT)及使用的哈希算法(如HMAC SHA256或RSA) 一个例子如下: 下边是Header部分的内容
{ "alg": "HS256", "typ": "JWT" }

将上边的内容使用Base64Url编码,得到一个字符串就是JWT令牌的第一部分。

  • Payload负载
    第二部分是负载,内容也是一个json对象,它是存放有效信息的地方,它可以存放jwt提供的现成字段,比 如:iss(签发者),exp(过期时间戳), sub(面向的用户)等,也可自定义字段。 此部分不建议存放敏感信息,因为此部分可以解码还原原始内容。
    最后将第二部分负载使用Base64Url编码,得到一个字符串就是JWT令牌的第二部分。 一个例子:
{ 
"sub": "1234567890", 
"name": "456", 
"admin": true 
}
  • Signature
    第三部分是签名,此部分用于防止jwt内容被篡改。 这个部分使用base64url将前两部分进行编码,编码后使用点(.)连接组成字符串,最后使用header中声明 签名算法进行签名。 一个例子:
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

base64UrlEncode(header):jwt令牌的第一部分。
base64UrlEncode(payload):jwt令牌的第二部分。
secret:签名所使用的密钥

分布式项目实现单点登录的案例代码

参考视频
参考视频

你可能感兴趣的:(java基础,微服务,SpringSecurity,JWT,单点登录,OAuth2.0,分布式认证)