本教程来自:尚硅谷SpringSecurity框架教程(spring security源码剖析从入门到精通)
教程分为五大部分:框架概述、入门和基本原理、基于Web的权限方案、基于微服务的权限方案、源码剖析,详细讲解了Spring Security框架,内容由浅入深,理论实践相结合,更深入源码级学习。
前置知识:javaweb,spring,springboot.
1 框架概述
1.1 概述
1.2 历史
1.3 竞品比较
1.3.1 SpringSecurity
1.3.2 shiro
apache的shiro:
1.3.3 总结
2 入门和基本原理
2.1 快速入门
使用springboot初始向导,在模块里选择web,security,lombok,创建好后parent改个版本,这里用2.2.1.RELEASE。
弄一个controller试试:
@RestController
@RequestMapping(value = "/test")
public class TestController {
@GetMapping(value = "/hello")
public String hello() {
return "hello security";
}
}
登录http://localhost:8080/test/hello,没有像预想的那样,跳hello,security,而是一个网址是http://localhost:8080/login的登录页面:
这说明spring security起效了,默认的带的用户是user,密码看后台输出。
输入用户密码后正常跳转了:
2.2 基本流程
参考:https://blog.csdn.net/u012702547/article/details/89629415
本质上是个过滤器链,很明显,过滤器链就是一种责任链模式,而且在过滤器组件注入容器的过程中肯定也用了代理模式。
流程:
- 客户端发起一个请求,进入 Security 过滤器链。
- 当到 LogoutFilter 的时候判断是否是登出路径,如果是登出路径则到 logoutHandler ,如果登出成功则到 logoutSuccessHandler 登出成功处理,如果登出失败则由 ExceptionTranslationFilter ;如果不是登出路径则直接进入下一个过滤器。
- 当到 UsernamePasswordAuthenticationFilter 的时候判断是否为登录路径,如果是,则进入该过滤器进行登录操作,如果登录失败则到 AuthenticationFailureHandler 登录失败处理器处理,如果登录成功则到 AuthenticationSuccessHandler 登录成功处理器处理,如果不是登录请求则不进入该过滤器。
- 当到 FilterSecurityInterceptor 的时候会拿到 uri ,根据 uri 去找对应的鉴权管理器,鉴权管理器做鉴权工作,鉴权成功则到 Controller 层否则到 AccessDeniedHandler 鉴权失败处理器处理。
对这条过滤器链的各个进行说明:
- WebAsyncManagerIntegrationFilter:将 Security 上下文与 Spring Web 中用于处理异步请求映射的 WebAsyncManager 进行集成。
- SecurityContextPersistenceFilter:在每次请求处理之前将该请求相关的安全上下文信息加载到 SecurityContextHolder 中,然后在该次请求处理完成之后,将 SecurityContextHolder 中关于这次请求的信息存储到一个“仓储”中,然后将 SecurityContextHolder 中的信息清除,例如在Session中维护一个用户的安全信息就是这个过滤器处理的。
- HeaderWriterFilter:用于将头信息加入响应中。
- CsrfFilter:用于处理跨站请求伪造。
- LogoutFilter:用于处理退出登录。
- UsernamePasswordAuthenticationFilter:用于处理基于表单的登录请求,从表单中获取用户名和密码。默认情况下处理来自 /login 的请求。从表单中获取用户名和密码时,默认使用的表单 name 值为 username 和 password,这两个值可以通过设置这个过滤器的usernameParameter 和 passwordParameter 两个参数的值进行修改。
- DefaultLoginPageGeneratingFilter:如果没有配置登录页面,那系统初始化时就会配置这个过滤器,并且用于在需要进行登录时生成一个登录表单页面。
- BasicAuthenticationFilter:检测和处理 http basic 认证。
- RequestCacheAwareFilter:用来处理请求的缓存。
- SecurityContextHolderAwareRequestFilter:主要是包装请求对象request。
- AnonymousAuthenticationFilter:检测 SecurityContextHolder 中是否存在 Authentication 对象,如果不存在为其提供一个匿名Authentication。
- SessionManagementFilter:管理 session 的过滤器
- ExceptionTranslationFilter:处理 AccessDeniedException 和 AuthenticationException 异常。
- FilterSecurityInterceptor:可以看做过滤器链的出口。
- RememberMeAuthenticationFilter:当用户没有登录而直接访问资源时, 从 cookie 里找出用户的信息, 如果 Spring Security 能够识别出用户提供的remember me cookie, 用户将不必填写用户名和密码, 而是直接登录进入系统,该过滤器默认不开启。
3 基于Web的权限方案
3.1 认证
当什么也没有配置的时候,账号和密码是由 Spring Security 定义生成的。
而在实际项目中账号和密码都是从数据库中查询出来的。 所以我们要通过自定义逻辑控制认证逻辑,只需要实现 UserDetailsService 接口即可。
在认证环节,还要用到一个别的接口,就是PasswordEncoder接口,该接口的实现类可以对密码进行加密、匹配判断等操作。
3.1.1 接口介绍:PasswordEncoder,UserDetailsService
(1) PasswordEncoder:
接口中方法介绍:
实现类:
BCryptPasswordEncoder 是 Spring Security 官方推荐的密码解析器,平时多使用这个解析器。
BCryptPasswordEncoder 是对 bcrypt 强散列方法的具体实现。是基于 Hash 算法实现的单向加密。可以通过 strength 控制加密强度,默认 10。
测试该实现类:
@Test
public void test01() {
// 创建密码解析器
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
// 对密码进行加密
String password = bCryptPasswordEncoder.encode("password");
// 打印加密之后的数据
System.out.println("加密之后数据:\t" + password);
//判断原字符加密后和加密之前是否匹配
boolean result = bCryptPasswordEncoder.matches("password", password);
// 打印比较结果
System.out.println("比较结果:\t" + result);
}
这里给个可复制的数据,这是password的密文,可以模拟从数据库里查出来的password的结果:
加密之后数据: $2a$10$AhI00zWwYSBj6Vb7AaxbSeIj2d4mjquILK.3IS4tCZQnC7b8p7hfS
比较结果: true
(2) UserDetailsService :
返回值UserDetails,也是个接口,是系统默认的用户主体:
实现这个UserDetail接口的有一个类User,用这个类就行:
User类有两个有参构造方法:
3.1.2 实现认证小demo
- 首先写一个实现UserDetailsService接口的认证实现类:
package com.atguigu.securitydemo1.service;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
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;
@Service("userDetailService")
public class MyUserDetailService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
//public User(String userName, String password, Collection<? extends GrantedAuthority> authorities)
// username:用户名,应该是传入的s
// password:从数据库读出来的加密的密码,这里模拟从数据库取出来,是"password"的加密结果
// authorities:权限信息
String userName = s;
String password = "$2a$10$QdIE.PKx7KWAd4dHbXDKOO6n5PXKH7AmL9ZCTEJ4Wv9yiHj6ja8Qq";
// 手工添加一份这个用户的授权信息
List<GrantedAuthority> admin = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
User user = new User(userName,password,admin);
return user;
}
}
- 然后将自定义的认证实现类和加密方式都注册到配置类中:
package com.atguigu.securitydemo1.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
//教程上这里有个@Bean,但私以为这个可不加,毕竟不是autowire注入方式使用,只在这里当个常规方法使用的话可不加,经验证不加也可以
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
输入localhost:8080/hello在登陆窗口输atguigu,atguigu,
可以进入hello页面就算成功。
3.1.3 带数据库(druid+mybatisplus)实现认证的案例
- 首先数据库弄好:
建个数据库learn_security,表名users.
建表语句:
CREATE TABLE `user`(
id BIGINT PRIMARY KEY AUTO_INCREMENT,
`user_name` VARCHAR(20) UNIQUE NOT NULL,
`password` VARCHAR(100)
);
INSERT INTO users VALUES(1,'userName1','$2a$10$AhI00zWwYSBj6Vb7AaxbSeIj2d4mjquILK.3IS4tCZQnC7b8p7hfS');
INSERT INTO users VALUES(2,'userName2','$2a$10$AhI00zWwYSBj6Vb7AaxbSeIj2d4mjquILK.3IS4tCZQnC7b8p7hfS');
其中密码模拟的是“password”加密后的。
- 然后mybatisplus的数据库操作接口弄好
依赖:
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--lombok 用来简化实体类-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.17</version>
</dependency>
配yaml:
spring:
datasource:
url: jdbc:mysql://localhost:3306/learn_security?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true
username: root
password: password
driver-class-name: com.mysql.cj.jdbc.Driver
druid:
aop-patterns: com.atguigu.securitydemo1.* #监控SpringBean
filters: stat,wall # 底层开启功能,stat(sql监控),wall(防火墙)
建users表的实体类:
import lombok.Data;
/**
*
* @TableName user
*/
@TableName(value ="user")
@Data
public class User implements Serializable {
/**
*
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
*
*/
private String userName;
/**
*
*/
private String password;
@TableField(exist = false)
private static final long serialVersionUID = 1L;
}
建UsersMapper接口,实现basemapper,自带基本的crud:
package com.atguigu.securitydemo1.mapper;
import com.atguigu.securitydemo1.entity.Users;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
//要么这里加@Mapper,要么启动类上加@MapperScan("com.atguigu.securitydemo1.mapper")
@Mapper
public interface UsersMapper extends BaseMapper<Users> {
}
- 然后实现UserDetailsService接口的认证类弄好
有两个user类,看好。
package com.atguigu.securitydemo1.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.coderhao.learn.entity.User;
import com.coderhao.learn.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
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 org.springframework.util.StringUtils;
import java.util.List;
@Service("userDetailService")
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(StringUtils.hasText(userName),User::getUserName,userName);
User user = userMapper.selectOne(queryWrapper);
if (user==null){
throw new UsernameNotFoundException("user not exist By userName:"+userName);
}
List<GrantedAuthority> authorityList = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
org.springframework.security.core.userdetails.User user1 = new org.springframework.security.core.userdetails.User(user.getUserName(), user.getPassword(), authorityList);
return user1;
}
}
实现WebSecurityConfigurerAdapter的SecurityConfig不用变,维持3.1.2节代码。
4. 登录测试
网址:http://localhost:8080/hello,弹出login页,输入正确的会跳到hello页就算成功,账号密码对不上会显示Bad credentials,不跳转:
3.1.4 自定义用户登录页面
上一节虽然实现了账号密码从数据库比对,但登录页是springsecurity自带的,我们要替换成自己的。
目录结构如下:
MySecurityConfig:
package com.atguigu.securityatguigu.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//自定义登陆页面
http.formLogin()
.loginPage("/login.html") //登录页
.loginProcessingUrl("/user/login") //登录数据访问的controller路径,这个路径不用配controller,是security自己在管理
.defaultSuccessUrl("/test/index").permitAll() //登陆成功后跳转的路径
.and().authorizeRequests()
.antMatchers("/","/test/hello","/user/login").permitAll() //设置不需要认证即可访问的路径
.anyRequest().authenticated()
.and().csrf().disable(); //关闭csrf防护
}
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
TestController:
package com.atguigu.securityatguigu.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/test")
public class TestController {
@GetMapping("/hello")
public String hello() {
return "hello,security";
}
@GetMapping("/index")
public String index() {
return "hello,index";
}
}
login.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h2>mylogin-page</h2>
<form action="/user/login" method="post">
username:<input type="text" name="username"><br>
password:<input type="password" name="password"><br>
<input type="submit" value="登录">
</form>
</body>
</html>
其他的参考上节案例。
3.2 授权
3.2.0 RBAC
每个用户的访问权限我们是通过给不同的用户赋予不同角色,角色不同,权限不同。
这就是为什么我们要给每个user一个角色,而不是单独规定user的访问权限。
一般我们会设计5个表:
- 用户表
用户名,密码,是否启用,是否锁定。。。。 - 角色表
角色名称,角色描述 - 用户-角色关联表
多对多关系 - 权限表
权限名,权限规定(比如可以访问的url等) - 权限-角色关联表
多对多关系
3.2.1 基于权限的访问控制
hasAuthority(String authority)
在MySecurityConfig中添加:
测试登陆正常。
但改变手动塞的权限后,就登不上了,报403,权限不足:
hasAnyAuthority(String… authorities)
hasAuthority(String authority)只能控制一种权限,当一个url多种权限都可以访问时,可以用hasAnyAuthority(String… authorities):
3.2.2 基于角色的访问控制
hasRole(String role)
hasAnyRole(String… roles)
使用和3.2.1节的两个方法一致
3.2.3 自定义403页面
3.3 web权限方案注解版
角色和权限控制的注解形式:
角色:
@Secured({“ROLE_admin”,“ROLE_normal”})
@PreAuthorize(“hasAnyRole(“ROLE_admin”,“ROLE_normal”)”)
权限:
@PreAuthorize(“hasAnyAuthority(“admin”,“normal”)”)
@PostAuthorize(“hasAnyAuthority(“admin”,“normal”)”) :使用并不多,在方法执行后再进行权限验证,适合验证带有返回值
的权限。
使用基于权限认证进行测试时,不要忘了改MyUserDetailService,去掉ROLE_前缀:
过滤:
建个index.html做测试:
修改MySecurityConfig:
登陆跳转设为index.html(不知道为什么这个在登陆后不起效。。。),
不需要认证的路径去掉“/”,
增加注解@EnableGlobalMethodSecurity(securedEnabled = true)
package com.atguigu.securityatguigu.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true)
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//自定义登陆页面
http.formLogin()
.loginPage("/login.html") //登录页
.loginProcessingUrl("/user/login") //登录数据访问的controller路径,这个路径不用配controller,是security自己在管理
.defaultSuccessUrl("/index.html").permitAll() //登陆成功后跳转的路径
.and().authorizeRequests()
.antMatchers( "/test/hello", "/user/login").permitAll() //设置不需要认证即可访问的路径
.anyRequest().authenticated()
.and().csrf().disable(); //关闭csrf防护
http.exceptionHandling().accessDeniedPage("/unauth.html");
}
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
TestController在原来基础上加两个路径:
@GetMapping("/admin")
@Secured({"ROLE_admin"})
public String admin(){
return "admin-page";
}
@GetMapping("/normal")
@Secured({"ROLE_admin","ROLE_normal"})
public String normal(){
return "normal-page";
}
测试登录role是normal的李si用户,发现点击admin的链接是自定义的403,normal可以正常进入。
3.4 补充:注销
3.5 session-cookie认证与token认证介绍
之前在web我们见过一种自动登录的实现形式—基于cookie,这里介绍通过另一种方式(JWT(json web token))实现自动登录。
参考:
Token登录认证详解
JWT的原理和使用
JWT(JSON Web Token)简介
(1)session-cookie:
没有cookie,session怎么运行:
(2)token-JWT(json web token):
token类型:
JWT组成:
3.6 csrf:跨站请求攻击
4 基于微服务的权限方案
认证里面很重要的一点就是单点登录,就是说一个用户在一个微服务模块登录后访问其他模块不需要登陆。
认证授权过程:
权限管理模型(RBAC方式):
4.1 案例实现
教学代码下载:https://download.csdn.net/download/anotherQu/49600330
sql代码可以取自:一个atguigu学生的gitee-springsecurity代码笔记
主要实现三点:
- 登录认证
- 添加用户
- 添加角色
- 为用户分配角色
- 为角色分配权限
使用技术:
- 后端:
maven:项目构建,父工程管理版本依赖,子模块使用依赖。
springboot:微服务的基本子模块
mybatisplus:数据库操作框架
springcloud技术: gateway网关、nacos注册中心
redis: nosql数据库
jwt:json web token,基于json、token的权限认证规范
swagger: 用来测试的东西 - 前端:
vue,vue-cli,element-ui
4.1.1 搭建项目工程
(1)模块搭建
教学代码下载:https://download.csdn.net/download/anotherQu/49600330
sql代码可以取自:一个atguigu学生的gitee-springsecurity代码笔记
(2)redis,nacos启动
redis和nacos的安装和启动请参考尚硅谷相关的视频教程。
- 启动redis,这里用windows版做演示,实际环境肯定是lunix版。
- 启动nacos注册中心
去官网上下个nacos1.1.4的源码:https://github.com/alibaba/nacos/tree/1.1.4
在解压目录下mvn处理源码:
命令:mvn -Prelease-nacos -DskipTests clean install -U
处理后得到nacos-server:
解压nacos-server后,点击bin下的 start.cmd运行nacos。
访问 http://localhost:8848/nacos/#/login 账号密码:nacos/nacos
(3)spring_security模块
其他模块不做说明,这里只展示security模块的相关代码
TokenWebSecurityConfig:
package com.atguigu.security.config;
import com.atguigu.security.filter.TokenAuthFilter;
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.UnauthEntryPoint;
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;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
//security总配置类
public class TokenWebSecurityConfig extends WebSecurityConfigurerAdapter {
private UserDetailsService userDetailsService;
private DefaultPasswordEncoder defaultPasswordEncoder;
private TokenManager tokenManager;
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;
}
//设置退出的地址和token,redis操作地址
@Override
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling()
//没有权限访问时调用自定义的处理类
.authenticationEntryPoint(new UnauthEntryPoint())
.and().csrf().disable()
.authorizeRequests()
.anyRequest().authenticated()
//退出路径
.and()
.logout().logoutUrl("/admin/acl/index/logout")
// 调用退出时的处理器
.addLogoutHandler(new TokenLogoutHandler(tokenManager, redisTemplate))
.and()
// 认证过滤器
.addFilter(new TokenLoginFilter(authenticationManager(), tokenManager, redisTemplate))
// 授权过滤器
.addFilter(new TokenAuthFilter(authenticationManager(), tokenManager, redisTemplate))
.httpBasic();
}
@Override
//调用userDetailsService和密码处理
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(defaultPasswordEncoder);
}
@Override
//不进行认证的路径,可以直接访问
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/api/**");
}
}
CurrentUserInfo:
package com.atguigu.security.entity;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
@Data
@ApiModel(description = "用户实体类")
public class CurrentUserInfo 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;
}
SecurityUser:
package com.atguigu.security.entity;
import lombok.Data;
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
public class SecurityUser implements UserDetails {
//当前登录用户
private transient CurrentUserInfo currentUserInfo;
//当前权限
private List<String> permissionValueList;
public SecurityUser() {
}
public SecurityUser(CurrentUserInfo 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;
}
}
TokenAuthFilter:
package com.atguigu.security.filter;
import com.atguigu.security.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 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;
//授权过滤器
@SuppressWarnings("unchecked")
public class TokenAuthFilter extends BasicAuthenticationFilter {
private TokenManager tokenManager;
private RedisTemplate redisTemplate;
public TokenAuthFilter(AuthenticationManager authenticationManager,
TokenManager tokenManager,
RedisTemplate redisTemplate) {
super(authenticationManager);
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain chain) throws IOException, ServletException {
// 获取当前认证成功的用户权限信息
UsernamePasswordAuthenticationToken authRequest = getAuthentication(request);
// 有权限信息 放入权限上下文中
if (authRequest != null) {
SecurityContextHolder.getContext().setAuthentication(authRequest);
}
chain.doFilter(request, response);
}
//从token获取用户名,从redis获取对应权限列表
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
//从header获取token
String token = request.getHeader("token");
if (token == null) {
return null;
}
//从token获取用户名
String username = tokenManager.getUserInfoFromToken(token);
//从redis获取对应权限列表
List<String> list = (List<String>) redisTemplate.opsForValue().get(username);
if (null == list) {
return null;
}
Collection<GrantedAuthority> authorities = new ArrayList<>();
list.forEach(item -> {
authorities.add(new SimpleGrantedAuthority(item));
});
return new UsernamePasswordAuthenticationToken(username, token, authorities);
}
}
TokenLoginFilter
package com.atguigu.security.filter;
import com.atguigu.security.entity.CurrentUserInfo;
import com.atguigu.security.entity.SecurityUser;
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.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;
//认证过滤器
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);
// 设置登陆路径,并且post请求
this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/acl/login", "POST"));
}
@Override
//获取表单提交的相关信息
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
// 获取表单提交数据
try {
CurrentUserInfo user = new ObjectMapper().readValue(request.getInputStream(), CurrentUserInfo.class);
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
user.getUsername(), user.getPassword(), new ArrayList<>()));
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException();
}
}
@Override
//认证成功调用的方法 生成token 存入到redis
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authResult) throws IOException, ServletException {
// 认证成功之后,得到认证成功后的用户信息
SecurityUser user = (SecurityUser) authResult.getPrincipal();
// 根据用户名生成token
String token = tokenManager.createToken(user.getUsername());
// 把用户名和用户权限放入redis中
redisTemplate.opsForValue().set(user.getCurrentUserInfo().getUsername(), user.getPermissionValueList());
ResponseUtil.out(response, R.ok().data("token", token));
}
@Override
//认证失败调用的方法
protected void unsuccessfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException failed) throws IOException, ServletException {
ResponseUtil.out(response, R.error());
}
}
DefaultPasswordEncoder:
package com.atguigu.security.security;
import com.atguigu.utils.utils.MD5;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
@Component
//密码处理工具类
public class DefaultPasswordEncoder implements PasswordEncoder {
public DefaultPasswordEncoder() {
this(-1);
}
public DefaultPasswordEncoder(int strength) {
}
//MD5加密
@Override
public String encode(CharSequence charSequence) {
return MD5.encrypt(charSequence.toString());
}
//密码比对
@Override
public boolean matches(CharSequence charSequence, String encodedPassword) {
return encodedPassword.equals(MD5.encrypt(charSequence.toString()));
}
@Override
public boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
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;
//退出处理器 退出时移除token 并删除redis中的token信息
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 httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) {
//1 从header里面获取token
//2 token不为空,移除token,从redis删除token
String token = httpServletRequest.getHeader("token");
if (token != null) {
//移除
tokenManager.removeToken(token);
//从token获取用户名
String username = tokenManager.getUserInfoFromToken(token);
redisTemplate.delete(username);
}
ResponseUtil.out(httpServletResponse, R.ok());
}
}
TokenManager:
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;
@Component
public class TokenManager {
//token有效时常 毫秒单位
private long tokenEcpiration = 24 * 60 * 60 * 1000;
//编码密钥,应该是自动生成,这里为了简单写死
private String tokenSignKey = "123456";
//根据用户名生成token
public String createToken(String username) {
String token = Jwts.builder()
.setSubject(username)
.setExpiration(new Date(System.currentTimeMillis() + tokenEcpiration))
.signWith(SignatureAlgorithm.HS512, tokenSignKey)
.compressWith(CompressionCodecs.GZIP)
.compact();
return token;
}
//根据token字符串得到用户信息
public String getUserInfoFromToken(String token) {
String userinfo = Jwts.parser()
.setSigningKey(tokenSignKey)
.parseClaimsJws(token)
.getBody()
.getSubject();
return userinfo;
}
//删除token
public void removeToken(String token) {
}
}
UnauthEntryPoint:
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;
public class UnauthEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
ResponseUtil.out(httpServletResponse, R.error());
}
}
(4)前端项目
根路径下:
npm install
npm run dev
(5)测试
前端启动:npm run dev
后端启动:nacos,redis,gateway模块,service_acl模块
访问:
http://localhost:9528