> 文章列表 > Spring Security源码剖析从入门到精通.跟学尚硅谷(二)

Spring Security源码剖析从入门到精通.跟学尚硅谷(二)

Spring Security源码剖析从入门到精通.跟学尚硅谷(二)

Spring Security源码剖析从入门到精通.跟学尚硅谷 二

  • 4. SpringSecurity 微服务权限方案
    • 4.1 什么是微服务
      • 1、微服务由来
      • 2、微服务优势
      • 3、微服务本质
    • 4.2 微服务认证与授权实现思路
      • 1、认证授权过程分析
      • 2、权限管理数据模型
      • 3、项目技术说明
      • 4、搭建工程
        • (1).创建父工程
        • (2).创建子模块common
        • (3).创建子模块infrastructure
        • (4).创建子模块service
      • 5.引入依赖
      • 6.启动redis和Nacos
      • 7.编写common工具类
      • 8.编写spring-security模块
    • 4.3 jwt 介绍
      • 1、访问令牌的类型
      • 2、JWT 的组成
        • JWT 头
        • 有效载荷
        • 签名哈希
        • Base64URL 算法
    • 4.4 具体代码实现
      • 4.4.1 编写核心配置类
      • 4.4.2 创建认证授权相关的工具类
      • 4.4.2 创建认证授权实体类
        • (1) SecutityUser
        • (2) User
      • 4.4.3 创建认证和授权的 filter
      • 4.4.4 添加aclservice包
      • 4.4.5 拷贝配置文件
      • 4.4.6 网关信息解决跨域问题
      • 4.4.7 前端修改
      • 4.4.7 启动项目
  • 5. SpringSecurity 原理总结
    • 5.1 SpringSecurity 的过滤器介绍
    • 5.2 SpringSecurity 基本流
    • 5.3 SpringSecurity 认证流程
      • 5.3.1 UsernamePasswordAuthenticationFilter 源码
      • 5.3.2 ProviderManager 源码
      • 5.3.3 认证成功/失败处理
    • 5.4 SpringSecurity 权限访问流程
      • 5.4.1 ExceptionTranslationFilter 过滤器
      • 5.4.2 FilterSecurityInterceptor 过滤器
    • 5.5 SpringSecurity 请求间共享认证信息
      • 5.5.1 SecurityContextPersistenceFilter 过滤器

4. SpringSecurity 微服务权限方案

Spring Security源码剖析从入门到精通.跟学尚硅谷(二)

4.1 什么是微服务

1、微服务由来

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

2、微服务优势

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

3、微服务本质

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

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

1、认证授权过程分析

(1)如果是基于 Session,那么 Spring-security 会对 cookie 里的 sessionid 进行解析,找到服务器存储的 session 信息,然后判断当前用户是否符合请求的要求。
⭐(2)如果是 token,则是解析出 token,然后将当前请求加入到 Spring-security 管理的权限信息中去。
在这里插入图片描述
在这里插入图片描述
如果系统的模块众多,每个模块都需要进行授权与认证,所以我们选择基于 token 的形式进行授权与认证,用户根据用户名密码认证成功,然后获取当前用户角色的一系列权限值,并以用户名为 key,权限列表为 value 的形式存入 redis 缓存中,根据用户名相关信息生成 token 返回,浏览器将 token 记录到 cookie 中,每次调用 api 接口都默认将 token 携带到 header 请求头中,Spring-security 解析 header 头获取 token 信息,解析 token 获取当前用户名,根据用户名就可以从 redis 中获取权限列表,这样 Spring-security 就能够判断当前请求是否有权限访问。

2、权限管理数据模型

在这里插入图片描述
这5张表的建表语句在资料中有提供:
在这里插入图片描述
创建数据库

CREATE DATABASE acldb;
USE acldb;

5张表关系如下:
在这里插入图片描述

3、项目技术说明

项目技术说明:
Spring Security源码剖析从入门到精通.跟学尚硅谷(二)

4、搭建工程

(1).创建父工程

Spring Security源码剖析从入门到精通.跟学尚硅谷(二)

创建Spring Boot新项目
Group:com.atguigu
Artifact:acl_parent
在这里插入图片描述
版本随便选,后面更改
在这里插入图片描述
修改pom.xml
在这里插入图片描述

<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.2.1.RELEASE</version><relativePath/> <!-- lookup parent from repository --></parent>

添加打包方式为pom

<!-- 打包方式 --><packaging>pom</packaging>

父工程不需要写代码,src目录可以删除
在这里插入图片描述

(2).创建子模块common

新建子模块
在这里插入图片描述
选择Maven
在这里插入图片描述
起名common
在这里插入图片描述
修改pom.xml
添加打包方式

<!-- 打包方式--><packaging>pom</packaging>

之后在common中新建两个子模块
在这里插入图片描述
新建Maven项目
在这里插入图片描述
Name:service_base

在这里插入图片描述
然后继续在common下创建Maven子项目
Name:spring_security
在这里插入图片描述
common里面也不用写代码,把src删除
在这里插入图片描述
创建完后查看pom.xml的结构多出来子模块
acl_parent的pom.xml
在这里插入图片描述
common的pom.xml
在这里插入图片描述

(3).创建子模块infrastructure

创建完毕后删除src目录
在这里插入图片描述
修改pom.xml
添加打包方式

<!-- 打包方式--><packaging>pom</packaging>

在infrastructure下创建子模块api_gateway

创建完后infrastructure的pom.xml中新增
在这里插入图片描述

(4).创建子模块service

创建完毕后删除src目录
在这里插入图片描述
修改pom.xml
添加打包方式

<!-- 打包方式--><packaging>pom</packaging>

在service下创建子模块service_acl

创建完后service的pom.xml中新增
在这里插入图片描述
然后看一下acl_parent的pom.xml中如下:
在这里插入图片描述
项目目录说明:
在这里插入图片描述

5.引入依赖

拷贝资料即可,把相关pom.xml文件内容复制即可
在这里插入图片描述

6.启动redis和Nacos

通过虚拟机启动,启动后查看
在这里插入图片描述

ps -ef | grep redis

启动Nacos注册中心

startup.cmd -m standalone

在这里插入图片描述
访问

http://localhost:8848/nacos/#/login

7.编写common工具类

在这里插入图片描述
新建包utils,然后拷贝上面内容即可
在这里插入图片描述
比如MD5类就是对密码加密,运行后查看结果
在这里插入图片描述
总结:
在这里插入图片描述

8.编写spring-security模块

见 4.4

4.3 jwt 介绍

1、访问令牌的类型

在这里插入图片描述

2、JWT 的组成

典型的,一个 JWT 看起来如下图:
在这里插入图片描述
该对象为一个很长的字符串,字符之间通过"."分隔符分为三个子串。
每一个子串表示了一个功能块,总共有以下三个部分:JWT 头、有效载荷和签名。

JWT 头

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

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

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

有效载荷

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

iss:发行人
exp:到期时间
sub:主题
aud:用户
nbf:在此之前不可用
iat:发布时间
jti:JWT ID 用于标识该 JWT

除以上默认字段外,我们还可以自定义私有字段,如下例:

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

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

签名哈希

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

Base64URL 算法

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

4.4 具体代码实现

主要看security包下的内容
在这里插入图片描述

4.4.1 编写核心配置类

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

TokenWebSecurityConfig

package com.atguigu.security.config;import com.atguigu.security.filter.TokenAuthenticationFilter;
import com.atguigu.security.filter.TokenLoginFilter;
import com.atguigu.security.security.DefaultPasswordEncoder;
import com.atguigu.security.security.TokenLogoutHandler;
import com.atguigu.security.security.TokenManager;
import com.atguigu.security.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.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;/*** @ClassName: TokenSecurityConfig* @Description: 核心配置类* @Author: wty* @Date: 2023/4/9*/
@Configuration
public class TokenWebSecurityConfig extends WebSecurityConfigurerAdapter {private TokenManager tokenManager;private RedisTemplate redisTemplate;private DefaultPasswordEncoder defaultPasswordEncoder;private UserDetailsService userDetailsService;@Autowiredpublic TokenWebSecurityConfig(UserDetailsService userDetailsService,DefaultPasswordEncoder defaultPasswordEncoder,TokenManager tokenManager,RedisTemplate redisTemplate) {this.userDetailsService = userDetailsService;this.defaultPasswordEncoder = defaultPasswordEncoder;this.tokenManager = tokenManager;this.redisTemplate = redisTemplate;}/*** 配置设置*///设置退出的地址和 token,redis 操作地址@Overrideprotected 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*/@Overridepublic void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userDetailsService).passwordEncoder(defaultPasswordEncoder);}/*** 配置哪些请求不拦截*/@Overridepublic void configure(WebSecurity web) throws Exception {web.ignoring().antMatchers("/api/**"");}
}

4.4.2 创建认证授权相关的工具类

(1)DefaultPasswordEncoder:密码处理的方法
在这里插入图片描述
DefaultPasswordEncoder.java

package com.atguigu.security.security;import com.atguigu.utils.utils.MD5;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;/*** @ClassName: DefaultPasswordEncoder* @Description: 密码处理的工具类* @Author: wty* @Date: 2023/4/9*/
@Component
public class DefaultPasswordEncoder implements PasswordEncoder {public DefaultPasswordEncoder() {this(-1);}public DefaultPasswordEncoder(int strength) {}/*** @param* @return java.lang.String* @description //进行MD5加密* @param: rawPassword* @date 2023/4/9 12:36* @author wty**/@Overridepublic String encode(CharSequence rawPassword) {return MD5.encrypt(rawPassword.toString());}/*** @param* @return boolean* @description // 进行密码的比对* @param: rawPassword  加密之前的密码* @param: encodedPassword 加密之后的密码* @date 2023/4/9 12:36* @author wty**/@Overridepublic boolean matches(CharSequence rawPassword, String encodedPassword) {return encodedPassword.equals(MD5.encrypt(rawPassword.toString()));}
}

(2)TokenManager:token 操作的工具类

package com.atguigu.security.security;import io.jsonwebtoken.CompressionCodecs;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.stereotype.Component;import java.util.Date;/*** @ClassName: TokenManager* @Description: Token操作的工具类* @Author: wty* @Date: 2023/4/9*/
@Component
public class TokenManager {/*** token的有效时长*/private long tokenEcpiration = 24 * 60 * 60 * 1000;/*** 编码密钥*/private String tokenSignKey = "123456";/*** @param* @return java.lang.String* @description //使用jwt根据用户名生成token* @date 2023/4/9 12:47* @author wty**/public String createToken(String username) {String token = Jwts.builder().setSubject(username).setExpiration(new Date(System.currentTimeMillis() + tokenEcpiration)).signWith(SignatureAlgorithm.HS512,tokenSignKey).compressWith(CompressionCodecs.GZIP).compact();return token;}/*** @param* @return java.lang.String* @description //根据token字符串得到用户信息* @date 2023/4/9 12:53* @author wty**/public String getUserInfoFromToken(String token) {String userInfo = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token).getBody().getSubject();return userInfo;}/*** @param* @return void* @description //删除token,客户端不携带即可,无需声明* @param: token* @date 2023/4/9 12:55* @author wty**/public void removeToken(String token) {}
}

(3)TokenLogoutHandler:退出实现

package com.atguigu.security.security;import com.atguigu.utils.utils.R;
import com.atguigu.utils.utils.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;/*** @ClassName: TokenLogoutHandler* @Description: 退出处理器实现* @Author: wty* @Date: 2023/4/9*/public class TokenLogoutHandler implements LogoutHandler {private TokenManager tokenManager;private RedisTemplate redisTemplate;public TokenLogoutHandler(TokenManager tokenManager, RedisTemplate redisTemplate) {this.tokenManager = tokenManager;this.redisTemplate = redisTemplate;}/*** @param* @return void* @description //TODO* @param: request* @param: response* @param: authentication* @date 2023/4/9 12:58* @author wty**/@Overridepublic void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {// 1.从header里获取到token// 2.token不能为空,移除token,从redis删除tokenString token = request.getHeader("token");if (null != token) {// 移除tokenManager.removeToken(token);// 从token获取用户名String username = tokenManager.getUserInfoFromToken(token);redisTemplate.delete(username);}ResponseUtil.out(response, R.ok());}
}

(4)UnauthorizedEntryPoint:未授权统一处理

package com.atguigu.security.security;import com.atguigu.utils.utils.R;
import com.atguigu.utils.utils.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;/*** @ClassName: UnauthorizedEntryPoint* @Description: 未授权统一处理* @Author: wty* @Date: 2023/4/9*/public class UnauthorizedEntryPoint implements AuthenticationEntryPoint {@Overridepublic void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {ResponseUtil.out(response, R.error());}
}

4.4.2 创建认证授权实体类

在这里插入图片描述

(1) SecutityUser

package com.atguigu.security.entity;import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;import java.util.ArrayList;
import java.util.Collection;
import java.util.List;/*** @ClassName: SecurityUser* @Description:* @Author: wty* @Date: 2023/4/9*/
@Data
public class SecurityUser implements UserDetails {//当前登录用户private transient User currentUserInfo;//当前权限private List<String> permissionValueList;public SecurityUser() {}public SecurityUser(User user) {if (null != user) {this.currentUserInfo = user;}}@Overridepublic 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;}@Overridepublic String getPassword() {return currentUserInfo.getPassword();}@Overridepublic String getUsername() {return currentUserInfo.getUsername();}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return true;}
}

(2) User

package com.atguigu.security.entity;import io.swagger.annotations.ApiModel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;/*** @ClassName: User* @Description:* @Author: wty* @Date: 2023/4/9*/
@Data
@ApiModel(description = "用户实体类")
@AllArgsConstructor
@NoArgsConstructor
public class User {private String username;private String password;private String nickName;private String salt;private String token;
}

4.4.3 创建认证和授权的 filter

在这里插入图片描述

(1)TokenLoginFilter:认证的 filter

package com.atguigu.security.filter;import com.atguigu.security.entity.SecurityUser;
import com.atguigu.security.entity.User;
import com.atguigu.security.security.TokenManager;
import com.atguigu.utils.utils.R;
import com.atguigu.utils.utils.ResponseUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
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;/*** @ClassName: TokenLoginFilter* @Description: 用户认证* @Author: wty* @Date: 2023/4/9*/public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {private RedisTemplate redisTemplate;private TokenManager tokenManager;private AuthenticationManager authenticationManager;public TokenLoginFilter(AuthenticationManager authenticationManager, TokenManager tokenManager, RedisTemplate redisTemplate) {this.authenticationManager = authenticationManager;this.tokenManager = tokenManager;this.redisTemplate = redisTemplate;this.setPostOnly(false);this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("admin/acl/login", "POST"));}/*** @param* @return org.springframework.security.core.Authentication* @description // 获取表单提交用户名和密码* @param: request* @param: response* @date 2023/4/9 13:09* @author wty**/@Overridepublic Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {// 获取表单提交的数据try {User user = new ObjectMapper().readValue(request.getInputStream(), User.class);return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), new ArrayList<>()));} catch (IOException e) {e.printStackTrace();throw new RuntimeException();}}/*** @param* @return void* @description // 认证成功* @param: request* @param: response* @param: chain* @param: authResult* @date 2023/4/9 13:09* @author wty**/@Overrideprotected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {// 认证成功,得到认证成功之后用户信息SecurityUser user = (SecurityUser) authResult.getPrincipal();//根据用户名生成tokenString token = tokenManager.createToken(user.getCurrentUserInfo().getUsername());// 把用户名称和用户权限列表放到redisredisTemplate.opsForValue().set(user.getCurrentUserInfo().getUsername(), user.getPermissionValueList());// 返回tokenResponseUtil.out(response, R.ok().data("token", token));}/*** @param* @return void* @description // 认证失败* @param: request* @param: response* @param: failed* @date 2023/4/9 13:09* @author wty**/@Overrideprotected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {ResponseUtil.out(response, R.error());}
}

(2)TokenAuthenticationFilter:授权 filter

package com.atguigu.security.filter;import com.atguigu.security.security.TokenManager;
import com.atguigu.utils.utils.R;
import com.atguigu.utils.utils.ResponseUtil;
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 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;/*** @ClassName: TokenAuthenticationFilter* @Description: 授权过滤器* @Author: wty* @Date: 2023/4/9*/public class TokenAuthenticationFilter extends BasicAuthenticationFilter {private TokenManager tokenManager;private RedisTemplate redisTemplate;public TokenAuthenticationFilter(AuthenticationManager authenticationManager, TokenManager tokenManager, RedisTemplate redisTemplate) {super(authenticationManager);this.tokenManager = tokenManager;this.redisTemplate = redisTemplate;}@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {logger.info("=================" + request.getRequestURI());if (request.getRequestURI().indexOf("admin") == -1) {chain.doFilter(request, response);return;}UsernamePasswordAuthenticationToken authenticationToken = null;try {// 获取当前认证成功用户权限信息authenticationToken = getAuthentication(request);} catch (Exception e) {ResponseUtil.out(response, R.error());}// 判断if (null != authenticationToken) {// 如果有权限信息,放到权限上下文中SecurityContextHolder.getContext().setAuthentication(authenticationToken);} else {ResponseUtil.out(response, R.error());chain.doFilter(request, response);}}private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {// token置于header里String token = request.getHeader("token");if (null != token) {// 从token中获取用户名String username = tokenManager.getUserInfoFromToken(token);// 从redis中获取对应的权限列表List<String> permissionValueList = (List<String>) redisTemplate.opsForValue().get(username);// 把permissionValueList转换成Collection<?extends GrantedAuthority>Collection<GrantedAuthority> authorityCollection = new ArrayList<>();for (String permissionValue : permissionValueList) {SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permissionValue);authorityCollection.add(authority);}return new UsernamePasswordAuthenticationToken(username, token, authorityCollection);}return null;}}

4.4.4 添加aclservice包

在这里插入图片描述
添加类UserDetailsServiceImpl.java
在这里插入图片描述

package com.atguigu.aclservice.service.impl;import com.alibaba.fastjson.JSONObject;
import com.atguigu.aclservice.entity.User;
import com.atguigu.aclservice.service.PermissionService;
import com.atguigu.aclservice.service.UserService;
import com.atguigu.security.entity.SecurityUser;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;import java.util.List;/*** @ClassName: UserDetailsServiceImpl* @Description:* @Author: wty* @Date: 2023/4/9*/
@Service("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {@Autowiredprivate PermissionService permissionService;@Autowiredprivate UserService userService;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// 根据用户名查询数据库User user = userService.selectByUsername(username);// 判断if (null == user) {throw new UsernameNotFoundException("用户不存在!");}User curUser = new User();BeanUtils.copyProperties(user, curUser);// 根据用户查询用户权限列表List<String> permissionValueList = permissionService.selectPermissionValueByUserId(user.getId());SecurityUser securityUser = new SecurityUser();securityUser.setPermissionValueList(permissionValueList);return securityUser;}
}

4.4.5 拷贝配置文件

在这里插入图片描述
从资料拷贝即可
在这里插入图片描述
相关配置文件更改成自己的即可。
在这里插入图片描述

4.4.6 网关信息解决跨域问题

新建包,并且新增类ApiGatewayApplication
在这里插入图片描述

package com.atguigu.gateway;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;/*** @ClassName: config* @Description:* @Author: wty* @Date: 2023/4/9*/
@SpringBootApplication
@EnableDiscoveryClient
public class ApiGatewayApplication {public static void main(String[] args) {SpringApplication.run(ApiGatewayApplication.class, args);}
}

创建配置类
在这里插入图片描述

CorsConfig

package com.atguigu.gateway.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import org.springframework.web.util.pattern.PathPatternParser;/*** @ClassName: CorsConfig* @Description:* @Author: wty* @Date: 2023/4/9*/
@Configuration
public class CorsConfig {/*** @param* @return org.springframework.web.cors.reactive.CorsWebFilter* @description //解决跨域问题* @date 2023/4/9 22:40* @author wty**/@Beanpublic CorsWebFilter corsWebFilter() {CorsConfiguration corsConfiguration = new CorsConfiguration();corsConfiguration.addAllowedMethod("*");corsConfiguration.addAllowedOrigin("*");corsConfiguration.addAllowedHeader("*");UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());source.registerCorsConfiguration("/**", corsConfiguration);return new CorsWebFilter(source);}
}

创建配置文件
在这里插入图片描述

application.properties

# 端口号
server.port=8222
# 服务名
spring.application.name=service-gateway
# 注册到nacos中
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
# 使用服务发现路由
spring.cloud.gateway.discovery.locator.enabled=true
# 配置路由规则
spring.cloud.gateway.routes[0].id=server-acl
# 设置路由uri lb://注册名称
spring.cloud.gateway.routes[0].uri=lb://server-acl
# 具体路由规则
spring.cloud.gateway.routes[0].predicates=Path=/*/acl/**

4.4.7 前端修改

修改dev.env.js
在这里插入图片描述
更改为网关路径
在这里插入图片描述

4.4.7 启动项目

先启动网关ApiGatewayApplication.java
然后启动服务类ServiceAclApplication.java

启动后登录

http://localhost:8848/nacos/

查看服务列表,发现有2个
在这里插入图片描述
启动前端项目
进入springsecurity-admin所在目录

在这里插入图片描述

执行下安装sass
比如我是node 12我就安装如下

npm install sass-loader@7.0.0 node-sass@4.12.0 --sass_binary_site=https://npm.taobao.org/mirrors/node-sass/

然后执行

npm run dev

之后会自动打开

http://localhost:9528/#/dashboard

在这里插入图片描述
账号和密码用页面底下提示的即可
登录后如图
在这里插入图片描述
查看redis,admin数据存入
在这里插入图片描述

5. SpringSecurity 原理总结

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

5.2 SpringSecurity 基本流

Spring Security 采取过滤链实现认证与授权,只有当前过滤器通过,才能进入下一个过滤器:
在这里插入图片描述
绿色部分是认证过滤器,需要我们自己配置,可以配置多个认证过滤器。认证过滤器可以使用 Spring Security 提供的认证过滤器,也可以自定义过滤器(例如:短信验证)。认证过滤器要在 configure(HttpSecurity http)方法中配置,没有配置不生效。下面会重点介绍以下三个过滤器:

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

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

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

5.3 SpringSecurity 认证流程

认证流程是在 UsernamePasswordAuthenticationFilter 过滤器中处理的,具体流程如下
所示:
在这里插入图片描述

Spring Security源码剖析从入门到精通.跟学尚硅谷(二)

5.3.1 UsernamePasswordAuthenticationFilter 源码

当前端提交的是一个 POST 方式的登录表单请求,就会被该过滤器拦截,并进行身份认证。该过滤器的 doFilter() 方法实现在其抽象父类
AbstractAuthenticationProcessingFilter 中,查看相关源码:
在这里插入图片描述
在这里插入图片描述

上述的第(2)过程调用了 UsernamePasswordAuthenticationFilter 的
attemptAuthentication() 方法,源码如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
上述的(3)过程创建的 UsernamePasswordAuthenticationToken 是
Authentication 接口的实现类,该类有两个构造器,一个用于封装前端请求传入的未认证的用户信息,一个用于封装认证成功后的用户信息:
在这里插入图片描述
Authentication 接口的实现类用于存储用户认证信息,查看该接口具体定义:
在这里插入图片描述

5.3.2 ProviderManager 源码

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

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

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

5.3.3 认证成功/失败处理

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

在这里插入图片描述
查看successfulAuthentication()和unsuccessfulAuthentication()方法源码:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5.4 SpringSecurity 权限访问流程

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

5.4.1 ExceptionTranslationFilter 过滤器

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

5.4.2 FilterSecurityInterceptor 过滤器

FilterSecurityInterceptor 是过滤器链的最后一个过滤器,该过滤器是过滤器链的最后一个过滤器,根据资源权限配置来判断当前请求是否有权限访问对应的资源。如果访问受限会抛出相关异常,最终所抛出的异常会由前一个过滤器ExceptionTranslationFilter 进行捕获和处理。具体源码如下:
在这里插入图片描述
在这里插入图片描述
需要注意,Spring Security 的过滤器链是配置在 SpringMVC 的核心组件
DispatcherServlet 运行之前。也就是说,请求通过 Spring Security 的所有过滤器,不意味着能够正常访问资源,该请求还需要通过 SpringMVC 的拦截器链。

5.5 SpringSecurity 请求间共享认证信息

Spring Security源码剖析从入门到精通.跟学尚硅谷(二)

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

在这里插入图片描述

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

在这里插入图片描述

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

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

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5.5.1 SecurityContextPersistenceFilter 过滤器

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

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

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

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述