SpringSecurity 实战
第一章 权限管理
-
权限管理
-
SpringSecurity 简介
-
整体架构
权限管理
基本上涉及到用户参与的系统都要进行权限管理,权限管理属于系统安全的范畴,权限管理 实现对用户访问系统的控制,按照安全规则或者安全策略控制用户可以访问而且只能访问自己被授权的资源。
权限管理包括⽤户身份认证和授权两部分,简称认证授权。对于需要访问控制的资源⽤户⾸ 先经过身份认证,认证通过后⽤户具有该资源的访问权限⽅可访问。
认证
进入移动互联网时代,大家每天都在刷手机,常用的软件有微信、支付宝、头条等,下边拿微信来举例子说明认证
相关的基本概念,在初次使用微信前需要注册成为微信用户,然后输入账号和密码即可登录微信,输入账号和密码
登录微信的过程就是认证。
系统为什么要认证?
身份认证,用户认证就是判断一个用户的身份是否合法的过程,用户去访问系统资源时系统要求验证用户的身份信
息,身份合法方可继续访问,不合法则拒绝访问。常见的用户身份认证方式有:用户名密码登录,二维码登录,手
机短信登录,指纹认证等方式。
授权
还拿微信来举例子,微信登录成功后用户即可使用微信的功能,比如,发红包、发朋友圈、添加好友等,没有绑定
银行卡的用户是无法发送红包的,绑定银行卡的用户才可以发红包,发红包功能、发朋友圈功能都是微信的资源即
功能资源,用户拥有发红包功能的权限才可以正常使用发送红包功能,拥有发朋友圈功能的权限才可以使用发朋友
圈功能,这个根据用户的权限来控制用户使用资源的过程就是授权。
为什么要授权?
认证是为了保证用户身份的合法性,授权则是为了更细粒度的对隐私数据进行划分,授权是在认证通过后发生的,
控制不同的用户能够访问不同的资源。
授权:授权是用户认证通过根据用户的权限来控制用户访问资源的过程,拥有资源的访问权限则正常访问,没有
权限则拒绝访问。
解决方案
和其他领域不同,在 Java 企业级开发中,安全管理框架非常少,目前比较常见的就是:
- Shiro
- Shiro 本身是⼀个老牌的安全管理框架,有着众多的优点,例如轻量、简单、易于集成、可以在JavaSE环境中使用等。不过,在微服务时代,Shiro 就显得力不从心了,在微服务⾯前和扩展方面,无法充分展示自己的优势。开发者自定义也有很多公司选择自定义权限,即自己开发权限管理。但是⼀个系统的安全,不仅仅是登录和权限控制这么简单,我们还要考虑种各样可能存在的网络政击以及防彻策略,从这个角度来说,开发者自己实现安全管理也并非是⼀件容易的事情,只有大公司才有足够的人力物力去支持这件事情。
- Spring Security
- Spring Security,作为spring 家族的⼀员,在和 Spring 家族的其他成员如 Spring Boot Spring Clond等进行整合时,具有其他框架无可比拟的优势,同时对 OAuth2 有着良好的支持,再加上Spring Cloud对 Spring Security的不断加持(如推出 Spring Cloud Security ),让 Spring Securiy 不知不觉中成为微服务项⽬的⾸选安全管理⽅案。
Spring Security历史
Spring Security 最早叫 Acegi Security, 这个名称并不是说它和 Spring 就没有关系,它依然是为Spring 框架提供安全支持的。Acegi Security 基于 Spring,可以帮助我们为项目建立丰富的角色与权限管理系统。Acegi security 虽然好用,但是最为人诟病的则是它臃肿烦琐的配置这一问题最终也遗传给了 Spring Security。
Acegi Security 最终被并入 Spring Security 项目中,并于 2008 年4月发布了改名后的第⼀个版本 Spring Security 2.0.0,到目前为止,Spring Security 的最新版本已经到了 5.6.1。和Shiro相比,Spring Security重量级并且配置烦琐,直至今天,依然有人以此为理由而拒绝了解Spring Security。其实,自从 Spring Boot推出后,就彻底颠覆了传统了 JavaEE 开发,自动化配置让许多事情变得非常容易,包括Spring Security 的配置。在⼀个 Spring Boot 项目中,我们甚至只需要引入⼀个依赖,不需要任何额外配置,项目的所有接⼝就会被自动保护起来了。在Spring Cloud中,很多涉及安全管理的问题,也是⼀个 Spring Security 依赖两行配置就能搞定,在和Spring 家族的产品⼀起使用时,Spring Security 的优势就非常明显了。因此,在微服务时代,我们不需要纠结要不要学习 Spring Security,我们要考虑的是如何快速掌握Spring Security,并且能够使用 Spring Security 实现我们微服务的安全管理。
整体架构
在的架构设计中,认证和授权 是分开的,⽆论使⽤什么样的认证⽅式。都不会影响授权,这是两个独⽴的存在,这种独⽴带来的好处之⼀,就是可以⾮常⽅便地整合⼀些外部的解决⽅案。
认证流程
spring Security功能的实现主要是由一系列过滤器链相互配合完成。
下面介绍过滤器链中主要的几个过滤器及其作用:
1.SecurityContextPersistenceFilter 是整个拦截过程的入口和出口(也就是第一个和最后一个拦截器).
会在请求开始时从配置好的 SecurityContextRepository 中获取 SecurityContext,然后把它设置给SecurityContextHolder。在请求完成后将 SecurityContextHolder 持有的 SecurityContext 再保存到配置好的 SecurityContextRepository,同时清除 securityContextHolder 所持有的 SecurityContext;
2.UsernamePasswordAuthenticationFilter 用于处理来自表单提交的认证。该表单必须提供对应的用户名和密码,其内部还有登录成功或失败后进行处理的 AuthenticationSuccessHandler和AuthenticationFailureHandler,这些都可以根据需求做相关改变;
3.FilterSecurityInterceptor 用于保护web资源,使用AccessDecisionManager对当前用户进行授权访问,前面已经详细介绍过了;
4.ExceptionTranslationFilter 捕获来自 FilterChain 所有的异常,并进行处理。但是它只会处理两类异常:
5.AuthenticationException 和 AccessDeniedException,其它的异常它会继续抛出。
让我们仔细分析认证过程:
- 用户提交用户名、密码被SecurityFilterChain中的 UsernamePasswordAuthenticationFilter 过滤器获取到,
封装为请求Authentication,通常情况下是UsernamePasswordAuthenticationToken这个实现类。
-
然后过滤器将Authentication提交至认证管理器(AuthenticationManager)进行认证
-
认证成功后, AuthenticationManager 身份管理器返回一个被填充满了信息的(包括上面提到的权限信息,
身份信息,细节信息,但密码通常会被移除) Authentication 实例。
- SecurityContextHolder 安全上下文容器将第3步填充了信息的 Authentication,通过
SecurityContextHolder.getContext().setAuthentication(…)方法,设置到其中。
可以看出AuthenticationManager接口(认证管理器)是认证相关的核心接口,也是发起认证的出发点,它
的实现类为ProviderManager。而SpringSecurity支持多种认证方式,因此ProviderManager维护着一个
List 列表,存放多种认证方式,最终实际的认证工作是由
AuthenticationProvider完成的。咱们知道web表单的对应的AuthenticationProvider实现类为
DaoAuthenticationProvider,它的内部又维护着一个UserDetailsService负责UserDetails的获取。最终
AuthenticationProvider将UserDetails填充至Authentication。
AuthenticationManager
在Spring Security中认证是由AuthenticationManager接⼝来负责的,接⼝定义为:
-
返回 Authentication 表示认证成功
-
返回 AuthenticationException 异常,表示认证失败。
AuthenticationManager 主要实现类为 ProviderManager,在 ProviderManager 中管理了众多AuthenticationProvider 实例。
在⼀次完整的认证流程中,SpringSecurity 允许存在多个AuthenticationProvider ,⽤来实现多种认证⽅式,这些AuthenticationProvider 都是由ProviderManager 进⾏统⼀管理的。
Authentication
认证以及认证成功的信息主要是由 Authentication 的实现类进⾏保存的,其接⼝定义为:
-
getAuthorities 获取用户权限信息
-
getCredentials 获取用户凭证信息,⼀般指密码
-
getDetails 获取用户详细信息
-
getPrincipal 获取用户身份信息,用户名、用户对象等
-
isAuthenticated 用户是否认证成功
SecurityContextHolder
SecurityContextHolder 用来获取登录之后用户信息。Spring Security 会将登录用户数据保存在 Session 中。但是,为了使用⽅便,Spring Security在此基础上还做了⼀些改进,其中最主要的⼀个变化就是线程绑定。当用户登录成功后,Spring Security 会将登录成功的用户信息保存到 SecurityContextHolder 中。
SecurityContextHolder 中的数据保存默认是通过ThreadLocal 来实现的,使用ThreadLocal 创建的变量只能被当前线程访问,不能被其他线程访问和修改,也就是用户数据和请求线程绑定在⼀起。当登录请求处理完毕后,Spring Security 会将SecurityContextHolder 中的数据拿出来保存到 Session 中,同时将SecurityContexHolder 中的数据清空。以后每当有请求到来时,Spring Security就会先从 Session 中取出⽤户登录数据,保存到 SecurityContextHolder 中,⽅便在该请求的后续处理过程中使用,同时在请求结束时将 SecurityContextHolder 中的数据拿出来保存到 Session 中,然后将 Security SecurityContextHolder 中的数据清空。这⼀策略⾮常⽅便⽤户在 Controller、Service 层以及任何代码中获取当前登录⽤户数据。
授权流程
当完成认证后,接下来就是授权了。在 Spring Security 的授权体系中,有两个关键接⼝:
分析授权流程:
- 拦截请求,已认证用户访问受保护的web资源将被SecurityFilterChain中的 FilterSecurityInterceptor 的子
类拦截。
- 获取资源访问策略,FilterSecurityInterceptor会从 SecurityMetadataSource 的子类
DefaultFilterInvocationSecurityMetadataSource 获取要访问当前资源所需要的权限
Collection 。
SecurityMetadataSource其实就是读取访问策略的抽象,而读取的内容,其实就是我们配置的访问规则,
读取访问策略如:
- 最后,FilterSecurityInterceptor会调用 AccessDecisionManager 进行授权决策,若决策通过,则允许访问资
源,否则将禁止访问。
AccessDecisionManager(访问决策管理器)的核心接口如下:
这里着重说明一下decide的参数:
authentication:要访问资源的访问者的身份
object:要访问的受保护资源,web请求对应FilterInvocation
confifigAttributes:是受保护资源的访问策略,通过SecurityMetadataSource获取。
decide 接口就是用来鉴定当前用户是否有访问对应受保护资源的权限。
AccessDecisionManager
AccessDecisionManager (访问决策管理器),用来决定此次访问是否被允许。
AccessDecisionVoter
AccessDecisionVoter (访问决定投票器),投票器会检查⽤户是否具备应有的⻆⾊,从⽽投出赞成、反对或者弃权票。
AccesDecisionVoter 和 AccessDecisionManager 都有众多的实现类,在AccessDecisionManager 中会换个遍历 AccessDecisionVoter,进⽽决定是否允许⽤户访问,因⽽ AaccesDecisionVoter 和 AccessDecisionManager 两者的关系类似于 AuthenticationProvider 和 ProviderManager 的关系。
ConfigAttribute
ConfigAttribute,用来保存授权时的⻆⾊信息
在 Spring Security 中,用户请求⼀个资源(通常是⼀个接⼝或者⼀个 Java ⽅法)需要的角色会被封装成⼀个 ConfigAttribute 对象,在 ConfigAttribute 中只有⼀个getAttribute方法,该方法返回⼀个 String 字符串,就是角色的名称。⼀般来说,角色名称都带有⼀个ROLE_前缀,投票器 AccessDecisionVoter 所做的事情,其实就是⽐较⽤户所具各的角色和请求某个资源所需的 ConfigAtuibute 之间的关系。
第⼆章 环境搭建&入门
环境搭建
-
spring boot
-
spring security
- 认证: 判断用户是否是系统合法⽤户过程
- 授权: 判断系统内用户可以访问或具有访问那些资源权限过程
1.创建springboot 应用导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
@RestController
@RequestMapping("/hello")
public class HelloController {
@GetMapping
public String hello(){
System.out.println("hello!");
return "hello";}}
2.启动项⽬
- 1.启动完成后控制台⽣成⼀个密码
- 2.访问 hello 发现直接跳转到登录⻚⾯
3.登录系统
- 默认⽤户名为: user
- 默认密码为: 控制台打印的 uuid
4.原理
-
请求 /hello 接⼝,在引⼊ spring security 之后会先经过⼀些列过滤器。
-
在请求到达 FilterSecurityInterceptor时,发现请求并未认证。请求拦截下来,并抛出 AccessDeniedException 异常。
-
抛出 AccessDeniedException 的异常会被 ExceptionTranslationFilter 捕获,这个 Filter 中会调用 LoginUrlAuthenticationEntryPoint中的commence⽅法给客户端返回 302,要求客户端进⾏重定向到 /login ⻚⾯。
-
客户端发送 /login 请求。
-
/login 请求会再次被拦截器中 DefaultLoginPageGeneratingFilter 拦截,并在拦截器中返回⽣成登录⻚⾯。
5.SpringSecurity过滤器
可以看出,Spring Security 提供了 30 多个过滤器。默认情况下Spring Boot 在对Spring Security 进⼊⾃动化配置时,会创建⼀个名为 SpringSecurityFilerChain 的过滤器,并注⼊到 Spring 容器中,这个过滤器将负责所有的安全管理,包括⽤户认证、授权、重定向到登录⻚⾯等。具体可以参考WebSecurityConfiguration的源码:
第三章 认证实战
- Spring Security认证 授权(默认)
- 数据库存储用户信息
- 密码加密配置
- 自定义登录页面
- 自定义认证模块
- JWT
- 前后端认证数据存储和校验
- 退出登录
- 自定义异常
3.1 Security认证授权(默认)
1.添加依赖SpringSecurity
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--redis依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--fastjson依赖-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.33</version>
</dependency>
<!--jwt依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
3.2 数据库存储用户信息
数据导入:
1.数据库sp_security,导入测试数据sp_security.sql
2.application.yml
3.导入User类
4.导入Usermapper/Usermapper.xml/Userservice 文件
自定义认证流程分析
请求发送的时候被拦截 调用MyUserDetailsService优先查询正确的账号密码
存入到LoginUser对象中方便后续的登陆比对。
框架会把用户输入的用户名和密码存入内存,我们需要重写用户名获取密码方式:
-
封装用户数据(LoginUser),交给 security,与框架内存中的用户名密码进行比较响应
-
创建MyUserDetailsService implements UserDetailsService并 重写方法loadUserByUsername
-
通过用户名从数据库获用户信息
-
判断用户信息数据库中是否存在
- 不存在:就抛出异常
- 存在:获取用户权限信息(后面完善)
代码:
MyUserDetailsService
@Component
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//封装成UserDetails对象返回
LoginUser loginUser = new LoginUser();
//1. 通过用户名从数据库获用户信息
User user = userMapper.selectUserByUsername(username);
//2. 判断用户信息数据库中是否存在
if (Objects.isNull(user)){
//3. 不存在:就抛出异常
throw new RuntimeException("用户名或密码错误");
}
//4. 存在:获取用户权限信息(后面完善)
// 封装用户信息 进行 认证
loginUser.setUser(user);
return loginUser;
}
}
LoginUser
(存储登录用户的信息)
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser implements UserDetails {
//传递user
private User user;
...
}
3.3 密码加密配置
@Slf4j
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
/*生成 对应策略的密码 可以进行数据库密码的更换*/
public static void main(String[] args) {
BCryptPasswordEncoder b = new BCryptPasswordEncoder();
String encode = b.encode("123456");
System.out.println(encode);
}
}
3.4 自定义登录页面
- 导入资料中前端页面信息
- 配置security 配置,放行页面
SecurityConfig
/*Security配置*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//关闭跨域校验
.csrf().disable()
//授权
.authorizeRequests()
//放行请求
.antMatchers(
"/login.html"
,"/js/**"
,"/logout"
,"/imgs/**"
,".html"
,"/auth/login"
, "/pages/**"
).permitAll()
// 任何请求都需要认证
.anyRequest().authenticated();
}
导入前端资料到static文件夹下
测试前端
3.5 自定义认证模块
1.导入vo/ResponseResult类
2.自定义登录认证接口 /auth/login,代替默认登录接口
AuthController
@RestController
@RequestMapping("/auth")
@Slf4j
public class AuthController {
@Autowired
private UserService userService;
@PostMapping("/login")
public ResponseResult login(@RequestBody User user){
log.info("user:{}",user);
ResponseResult result = userService.login(user);
return result;
}
}
UserService
public interface UserService {
ResponseResult login(User user);
}
UserServiceImpl
@Service
public class LoginServiceImpl implements LoginService {
@Autowired
private AuthenticationManager authenticationManager;
@Override
public ResponseResult login(User user) {
String username = user.getUsername();
String password = user.getPassword();
//1.AuthenticationManager authentication 进行用户认证
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
//交给权限框架进行认证 默认调用 loadUserByUsername方法
Authentication authenticate = authenticationManager.authenticate(authRequest);
//2.如果认证没有通过,给出对应的提示
if(Objects.isNull(authenticate)){
throw new RuntimeException("登录失败");
}
// 封装返回数据
return ResponseResult.fail(200,"登录成功","success");
}
}
SecurityConfig
认证AuthenticationManager bean对象
//认证AuthenticationManager bean对象
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
3.6 加密 JWT
- 导入依赖,导入JWT工具类
3.6.1 什么是JWT?
JSON Web Token(JWT)是一个开放的行业标准(RFC 7519),它定义了一种简介的、自包含的协议格式,用于 在通信双方传递json对象,传递的信息经过数字签名可以被验证和信任。JWT可以使用HMAC算法或使用RSA的公钥/私钥对来签名,防止被篡改。
官网:https://jwt.io/
标准:https://tools.ietf.org/html/rfc7519
JWT令牌的优点:
1)jwt基于json,非常方便解析。
2)可以在令牌中自定义丰富的内容,易扩展。
3)通过非对称加密算法及数字签名技术,JWT防止篡改,安全性高。
4)资源服务使用JWT可不依赖认证服务即可完成授权。
缺点:
1)JWT令牌较长,占存储空间比较大。
3.6.2 JWT令牌结构
通过学习JWT令牌结构为自定义jwt令牌打好基础。
JWT令牌由三部分组成,每部分中间使用点(.)分隔,比如:xxxxx.yyyyy.zzzzz
Header(头信息).Payload(用户信息).Signature(签名)
-
Header
头部包括令牌的类型(即JWT)及使用的哈希算法(如HMAC SHA256或RSA)
一个例子如下: 下边是Header部分的内容
{ "alg": "HS256", "typ": "JWT" }
将上边的内容使用Base64Url编码,得到一个字符串就是JWT令牌的第一部分。
-
Payload
第二部分是负载,内容也是一个json对象,它是存放有效信息的地方,它可以存放jwt提供的现成字段,比 如:iss(签发者),exp(过期时间戳), sub(面向的用户)等,也可自定义字段。 此部分不建议存放敏感信息,因为此部分可以解码还原原始内容。 最后将第二部分负载使用Base64Url编码,得到一个字符串就是JWT令牌的第二部分。
一个例子:
{
"username": "zhangsan",
"password": "456"
...
}
-
Signature
第三部分是签名,此部分用于防止jwt内容被篡改。 这个部分使用base64url将前两部分进行编码,编码后使用点(.)连接组成字符串,最后使用header中声明 签名算法进行签名。
一个例子:
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
base64UrlEncode(header):jwt令牌的第一部分。
base64UrlEncode(payload):jwt令牌的第二部分。
secret:签名所使用的密钥。
3.6.3 案例
依赖
<!-- JWT依赖 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
public class JwtApp {
private static String yan = "itgaohe";
public static void main(String[] args) {
// 使用JWT自带的构造器构造一个jwt
JwtBuilder builder = Jwts.builder();
String token = builder
//封装header属性
.setHeaderParam("typ","JWT")
.setHeaderParam("slg","HS256")
//封装payload里的信息
.claim("username","zhangsan")
.claim("name","张三")
.claim("age","18")
//在payload中设置一个超时时间 毫秒
.setExpiration(new Date(System.currentTimeMillis()+Long.valueOf(1000*60*60*1)))
.setId("1")
//构造signature部分
.signWith(SignatureAlgorithm.HS256,yan)
//构造签名
.compact();
System.out.println(token);
System.out.println("===========开始解密============");
JwtParser parser = Jwts.parser();
Jws<Claims> claimsJws = parser.setSigningKey(yan).parseClaimsJws(token);
Claims body = claimsJws.getBody();
System.out.println("name"+body.get("name"));
System.out.println("age"+body.get("age"));
System.out.println("username"+body.get("username"));
System.out.println(claimsJws);
}
}
login–创建令牌—发送到前台(把令牌放到cookie中)—每次发起请求把令牌组装到请求头里面
获取请求头–获取令牌–获取用户id–通过id获取用户信息–获取权限信息
权限字符串比对(包含相关路径权限—true)
3.7 前后端认证数据存储校验
步骤:
1.导入redis依赖,导入工具类RedisCache,导入RedisConfig
2.导入 jwt工具类 JwtUtil jwt依赖
2.把登录的id和对应用户权限信息存入redis
3.7.1 发送redis缓存的权限信息并向前台发送token
UserServiceImpl
@Service
public class UserServiceImpl implements UserService {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private RedisCache redisCache;
@Override
public ResponseResult login(User user) {
String username = user.getUsername();
String password = user.getPassword();
//1.AuthenticationManager authentication 进行用户认证 和loginUser中的进行比对
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
//交给权限框架进行认证 默认调用 loadUserByUsername方法
Authentication authenticate = authenticationManager.authenticate(authRequest);
//2.如果认证没有通过,给出对应的提示
if (Objects.isNull(authenticate)) {
throw new RuntimeException("登录失败");
}
//3.如果认证通过了获取权限 和userid生成一个jwt 存入返回结果集
// authenticate表示一个身份验证对象,。
// getprincipal() 表示获取验证对象的主体
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
// 把完整的用户信息存入redis userid 作为key loginUser 作为value
String userid = loginUser.getUser().getId().toString();
String key = "login:" + userid;
// redisCache.setCacheObject是一个用于将数据存储到Rediscache中的方法。它接受两个参数:key和value。key是用于唯一标识数据的键,而value则是要存储的数据。
redisCache.setCacheObject(key, loginUser, 30, TimeUnit.MINUTES);
//利用jwt创建token 发送给前端在前端session空间中做存储
String jwtId = JwtUtil.createJWT(userid);
//封装token 返回给结果集合的数据中
HashMap<String, String> map = new HashMap<>();
map.put("token", jwtId);
// 封装返回数据
return ResponseResult.success(200, "登陆成功", map);
}
}
3.7.2 关闭security 默认的session认证
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf()
.disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and()
...}
3.7.3自定义授权过滤器
1.获取前端请求头中存储的token 放行 登录请求等 放行
2.解析前端存储的token 返回对应的用户id
3.将用户id做为key从redis中查找权限信息
4.将权限信息封装存入到SecurityContextHolder
5.放行
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private RedisCache redisCache;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//获取token
String token = request.getHeader("token");
//放行 登录请求等 放行
//使用FilterChain的doFilter方法可以放行拦截请求
if(!StringUtils.hasText(token)){
filterChain.doFilter(request,response);
return;
}
//解析前端存储的token
String userid;
try {
Claims claims = JwtUtil.parseJWT(token);
userid = claims.getSubject();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("token非法");
}
// 拼接redis中 存储的token的key login:3
String redisKey = "login:"+userid;
// 从redis中获取对应的权限信息
LoginUser loginUser = redisCache.getCacheObject(redisKey);
if(Objects.isNull(loginUser)){
throw new RuntimeException("用户未登录");
}
// 封装权限信息
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
loginUser,null,loginUser.getAuthorities());
//存入SecurityContextHolder
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//放行
filterChain.doFilter(request,response);
}
}
3.7.4 SecurityConfig中配置过滤器
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
// 配置认证过滤器
http.addFilterBefore(jwtAuthenticationTokenFilter,UsernamePasswordAuthenticationFilter.class);
}
3.8 debug测试
1.登录 先进入到login中 当请求要执行的时候 被springsecurity拦截 先查询数据库中正确的账号密码并存入到loginuser中
2.放行登录逻辑 将输入的账号密码和loginuser中的比对 如果一致则认证通过,存入到redis中 否则 失败。
3.9 退出登录
- 放行退出登录接口
- 把redis中数据删除
@GetMapping("/logout")
public ResponseResult logout(HttpServletRequest request) throws Exception {
String token = request.getHeader("token");
Claims claims = JwtUtil.parseJWT(token);
String userid = claims.getSubject();
log.info("token:{}",token);
ResponseResult result = loginService.logout(userid);
return result;
}
ResponseResult logout(String userid);
@Override
public ResponseResult logout(String userid) {
redisCache.deleteObject("login:"+userid);
return ResponseResult.succ(200,"注销成功",userid);
}
3.10 自定义异常
ss框架 如果密码错误不会报错! 会给页面响应401
1.导入WebUtils工具类
- 自定义认证异常
- 自定义授权异常
/*Security配置*/
@Override
protected void configure(HttpSecurity http) throws Exception {
...
// 任何请求都需要认证
.anyRequest().authenticated()
.and()
//开启自定义认证授权异常响应
.exceptionHandling()
//自定义认证异常响应
.authenticationEntryPoint((req, resp, ex) -> {
ResponseResult responseResult = ResponseResult
.fail403("403 未授权登录,请跳转登录页面登录");
WebUtils.renderString(resp,responseResult);
})
//自定义授权异常响应
.accessDeniedHandler((req, resp, ex) -> {
ResponseResult responseResult = ResponseResult
.fail401("401 无权限操作" + ex.getMessage());
WebUtils.renderString(resp,responseResult);
});
}
第四章 授权实战
授权,即访问控制,控制谁能访问哪些资源。简单的理解授权就是根据系统提前设置好的规则,给用户分配可以访问某一个资源的权限,用户根据自己所具有的权限,去执行相应操作。
认证成功之后会将当前登录用户信息保存到Authentication对象中,Authentication对象中有一个getAuthorities()方法,用来返回当前登录用户具备的权限信息,也就是当前用户具有权限信息。该方法的返回值为Collection<? extends GrantedAuthority>,当需要进行权限判断时,就会根据集合返回权限信息调用相应 方法进行判断。
那么问题来了,针对于这个返回值GrantedAuthority应该如何理解?是角色还是权限?
基于角色进行权限管理
基于资源进行权限管理 -->权限字符串
R(Role Resources) B(base) A(access) C(controll)
4.1 权限实现方式
-
基于角色权限设计就是: 用户—角色—资源 三者关系 返回就是用户的角色
-
基于资源权限设计就是: 用户—权限—资源 三者关系 返回就是用户的权限
-
基于角色和资源权限设计就是: 用户—角色—权限—资源 返回统称为用户的权限,为什么可以统称为权限,因为从代码层面角色和权限没有太大不同都是权限,特别是在
Spring Security 中,角色和权限处理方式基本上都是一样的。唯一区别
SpringSecurity 在很多时候会自动给角色添加一个ROLE_前缀,而权限则不会自动添加
4.2 权限管理策略
基于过滤器的权限管理主要是用来拦截 HTTP 请求,拦截下来之后,根据 HTTP请求地址进行权限校验。
基于 AOP (方法)的权限管理 (MethodSecurityInterceptor)基于 AOP 权限管理主要是用来处理方法级别的权限问题。当需要调用某⼀个方法时,通过 AOP 将操作拦截下来,然后判断用户是否具备相关的权限。
我们使用基于过滤器(URL)的权限管理 (FilterSecurityInterceptor)
基于过滤器的权限管理主要用来拦截HTTP请求,拦截下来之后,根据HTTP请求地址进行权限校验
4.3 实现
1.导入资料
1.1导入实体类Role和Permissions
1.2映射接口RoleMapper/PermissionsMapper
1.3映射文件RoleMapper.xml/PermissionsMapper.xml
1.4 PermissionsService PermissionsServiceImpl
2.修改用户对象实体类LoginUser,添加权限属性和封装
3.修改loadUserByUsername,获取权限,封装进用户对象实体类
4.JwtAuthenticationTokenFilter从redis中获取用户信息授权拦截
5.权限表达式,在接口层添加权限
2.修改用户对象实体类,添加权限属性和封装
/**
* 自定义类继承UserDetails 用于security框架认证封装
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser implements UserDetails {
...
//关系属性 用来存储当前用户所有角色信息
private List<Role> roles = new ArrayList<>();
private List<Permissions> permissions = new ArrayList<>();
// 把数据库中的角色转化为权限
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Set<SimpleGrantedAuthority> authorities = new HashSet<>();
roles.forEach(role -> {
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(role.getName());
authorities.add(simpleGrantedAuthority);
});
permissions.forEach(p -> {
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(p.getName());
authorities.add(simpleGrantedAuthority);
});
return authorities;
}
...
}
3.修改loadUserByUsername,获取权限,封装进用户对象实体类
查询的时候就差该用户所具有的权限
@Component
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Autowired
private RoleMapper roleMapper;
@Autowired
private PermissionsService permissionsService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
....
loginUser.setUser(user);
//查询权限信息并封装
//2.解析r_ids字符串 获取role_id集合,去role表查询角色
List<Integer> roleIds = Arrays
.stream(user.getRIds().split(","))
.map(Integer::parseInt)
.collect(Collectors.toList());
//3.获取用户对应 所有的角色信息
List<Role> roleList = roleMapper.selectBatchIds(roleIds);
loginUser.setRoles(roleList);
//4.调用权限业务层 获取用户所具有的权限
List<Permissions> permissionsList = permissionsService.selectByUsername(user.getUsername());
loginUser.setPermissions(permissionsList);
return loginUser;
}
}
RoleMapper
@Mapper
public interface RoleMapper {
List<Role> selectByIds(@Param("roleIds") List<Integer> roleIds);
}
RoleMapper.xml
<select id="selectByIds" resultType="com.itgaohe.pojo.Role" parameterType="list">
select * from role where id in
<foreach collection="roleIds" open="(" close=")" separator="," item="roleIds">
#{roleIds}
</foreach>
</select>
PermissionsServiceImpl
@Service
public class PermissionsServiceImpl implements PermissionsService {
@Autowired
private UserMapper userMapper;
@Autowired
private RoleMapper roleMapper;
@Autowired
private PermissionsMapper permissionsMapper;
@Override
public List<Permissions> selectByUsername(String username) {
//1.查询用户信息
User user = userMapper.selectUserByUsername(username);
if (user == null) {
throw new RuntimeException(String.format("用户'%s'不存在", username));
}
//2.解析r_ids字符串 获取role_id集合,去role表查询角色
List<Integer> roleIds = Arrays.stream(user.getR_ids().split(","))
.map(Integer::parseInt)
.collect(Collectors.toList());
//3.获取用户对应 所有的角色信息
List<Role> roles = roleMapper.selectByIds(roleIds);
//4.获取角色对应 所有的权限pids
List<Integer> pids = new ArrayList<>();
for (Role role : roles) {
List<Integer> permissionsIds = Arrays.stream(role.getP_ids().split(",")).map(Integer::parseInt).collect(Collectors.toList());
pids.addAll(permissionsIds);
}
//5.获取所有权限信息
List<Permissions> permissionsList = permissionsMapper.selectByPids(pids);
return permissionsList;
}
}
PermissionsMapper
@Mapper
public interface PermissionsMapper{
List<Permissions> selectByPids(@Param("pids") List<Integer> pids);
}
PermissionsMapper.xml
<select id="selectByPids" resultType="com.itgaohe.pojo.Permissions">
select * from permissions where id in
<foreach collection="pids" open="(" close=")" separator="," item="pids">
#{pids}
</foreach>
</select>
- 权限表达式
4.4 限制访问资源所需权限
SpringSecurity为我们提供了基于注解的权限控制方案,这也是我们项目中主要采用的方式。
我们使用注解去指定访问对应的资源所需的权限。
但是要使用它我们需要先开启相关配置。
@Configuration
@EnableWebSecurity // 开启Security服务
//会拦截注解了@PreAuthrize注解的配置.
@EnableGlobalMethodSecurity(prePostEnabled=true,securedEnabled=true)
public class SecurityConfig extends WebsecurityConfigurerAdapter{}
在web接口添加权限注解
@PreAuthorize(“hasAuthority(‘admin:select’)”)
AdminController
@RestController
@RequestMapping("/admin")
@Slf4j
public class AdminController {
@GetMapping
//@AuthenticationPrincipal注解是Spring Security框架提供的一个注解,用于获取当前用户的认证信息。它可以用于方法参数上,表示将当前用户的认证信息注入到该参数中。通常情况下,我们可以使用它来获取当前用户的用户名、角色、权限等信息,以便进行业务逻辑的处理。
public Object admin(@AuthenticationPrincipal UserDetails userDetails) {
log.info("userDetails={}",userDetails);
return userDetails;
}
@PreAuthorize("hasAuthority('admin:select')")
@GetMapping("/select")
public Object select() { return "Select"; }
@PreAuthorize("hasAuthority('admin:insert')")
@GetMapping("/create")
public Object create() { return "Create"; }
@PreAuthorize("hasAuthority('admin:update')")
@GetMapping("/update")
// hasAnyRole 匹配角色,自动加上ROLE_前缀
public Object update() { return "Update"; }
@PreAuthorize("hasAuthority('admin:delete')")
@GetMapping("/delete")
// hasAnyRole 匹配角色,自动加上ROLE_前缀
public Object delete() { return "Delete"; }
}
UserController
@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
@GetMapping
public Object user(@AuthenticationPrincipal UserDetails userDetails) {
log.info("userDetails={}",userDetails);
return userDetails;
}
@GetMapping("/select")
@PreAuthorize("hasAuthority('user:select')")
public Object select() { return "Select"; }
@GetMapping("/create")
@PreAuthorize("hasAnyAuthority({'user:insert','admin:insert'})")
// 两个权限任意一个就可以
public Object create() { return "Create"; }
@GetMapping("/update")
@PreAuthorize("hasAnyAuthority({'user:update','admin:update'})")
// hasAnyRole 匹配角色,自动加上ROLE_前缀
public Object update() { return "Update"; }
@GetMapping("/delete")
@PreAuthorize("hasAnyAuthority({'user:delete','admin:delete'})")
// hasAnyRole 匹配角色,自动加上ROLE_前缀
public Object delete() { return "Delete"; }
}
第五章 前端axios拦截器处理
该拦截器只会拦截axios请求和响应
axios.interceptors.request.use(req => {
//从空间中获取token
var token = sessionStorage.getItem("token")
if(token) {
//把token封装请求头上
req.headers.token = token
}
console.log("token:"+token)
return req;
})
//响应拦截器
axios.interceptors.response.use(resp => {
return resp;
}, (err) => {
console.log(err.response)
if (err.response.status == 403) {
alert(err.response.data.msg)
}
return err;
})
login.html
//把登录数据存储到sessionStorage空间中
sessionStorage.setItem("token",res.data.data.token);
home.html
<script src="../js/request.js"></script>