动力节点Springsecurity
- Springsecurity简介
- 不使用Springsecurity安全框架的springboot web程序
- 认证授权等基本概念
- java的安全框架实现
- 认证入门
- 密码处理
- 查看当前登录用户信息及配置用户权限
- 授权(对URL进行授权)
- 授权(方法级别的权限控制)
- SpringSecurity 返回json
- 使用自定义UserDetailsService实现获取用户认证信息
- 基于数据库的认证
- 基于数据库的方法授权
- SpringSecurity 集成thymeleaf
- springsecurity 集成图片验证码
- Base64 和JWT学习
- JWT+Spring Security+redis+mysql 实现认证
- 启动redis并使用客户端工具连接到redis
Springsecurity简介
Spring Security是一个开源的Java框架,用于实现应用程序的安全性和身份认证。它提供了一套强大的身份验证和权限管理功能,可用于保护Spring应用程序中的资源和API。官方网站
Spring Security的主要特性包括:
- 身份认证:通过各种认证方法(如表单登录、基本认证、OAuth等)验证用户身份。
- 访问控制:基于角色、权限或其他自定义规则,控制用户对资源和功能的访问。
- 用户会话管理:管理用户会话,包括登录和登出、会话超时等。
- 密码编码:提供密码编码器,以确保用户密码的安全存储和传输。
- Remember-Me功能:允许用户在长时间离线后仍然保持登录状态。
- CSRF(跨站请求伪造)保护:防止CSRF攻击。
- 支持多种认证方式:支持数据库、LDAP、OpenID等多种认证方式。
- 支持与Spring框架整合:可以与其他Spring框架一起使用,如Spring Boot、Spring MVC等。
使用Spring Security可以轻松地为Spring应用程序添加安全性,保护应用程序资源和API免受未经授权的访问。它是Java领域中最流行的安全框架之一,并被广泛用于企业级应用程序的开发中。
不使用Springsecurity安全框架的springboot web程序
新建三个controller
com.powernode.controller包下新建三个controller
学生
@RestController
@RequestMapping("/student")
public class StudentController {
@GetMapping("/query")
public String queryInfo(){
return "I am a student,My name is Eric!";
}
}
教师
@RestController
@RequestMapping("/teacher")
public class TeacherController {
@GetMapping("/query")
public String queryInfo(){
return "I am a teacher,My name is Thomas!";
}
}
管理员
@RestController
@RequestMapping("/admin")
public class AdminController {
@GetMapping("/query")
public String queryInfo(){
return "I am a administrator, My name is Obama!";
}
}
url访问
http://localhost:8080/student/query
http://localhost:8080/teacher/query
结论
此示例说明:
没有加入安全框架的SpringBoot web程序,默认所有资源均不受保护。
问题
我们的项目很多资源必须被保护起来,如何保护?引入安全框架
认证授权等基本概念
认证(authentication)
系统为什么要认证?
认证是为了保护系统的隐私数据与资源,用户的身份合法方可访问该系统的资源。
什么是认证(登录)?
认证 :用户认证就是判断一个用户的身份是否合法的过程。
常见的用户身份认证方式
Ø 用户名密码登录
Ø 二维码登录
Ø 手机短信登录
Ø 指纹认证
Ø 人脸识别
Ø 等等…
会话(session)
什么是会话
用户认证通过后,为了避免用户的每次操作都进行认证可将用户的信息保存在会话中。会话就是系统为了保持当前用户的登录状态所提供的机制,常见的有基于****session方式、基于token方式等。
基于session的认证方式
它的交互流程是,用户认证成功后,在服务端生成用户相关的数据保存在session(当前会话)中,发给客户端的sesssion_id 存放到 cookie 中,这样用户客户端请求时带上 session_id 就可以验证服务器端是否存在 session 数据,以此完成用户的合法校验,当用户退出系统或session过期销毁时,客户端的session_id也就无效了。
基于token的认证方式
它的交互流程是,用户认证成功后,服务端生成一个token发给客户端,客户端可以放到 cookie 或 localStorage等存储中,每次请求时带上 token,服务端收到token通过验证后即可确认用户身份。可以使用Redis 存储用户信息(分布式中共享session)。
基于session的认证方式由Servlet规范定制,服务端要存储session信息需要占用内存资源,客户端需要支持cookie;基于token的方式则一般不需要服务端存储token,并且不限制客户端的存储方式。如今移动互联网时代更多类型的客户端需要接入系统,系统多是采用前后端分离的架构进行实现,所以基于token的方式更适合。
授权(authorization)
为什么要授权(控制资源被访问)?
因为不同的用户可以访问的资源是不一样的。
什么是授权(给用户颁发权限)
授权: 授权是用户认证通过后,根据用户的权限来控制用户访问资源的过程。
拥有资源的访问权限则正常访问,没有权限则拒绝访问。
RBAC(Role-Based Access Control) 基于角色的访问控制
用户,角色,权限 本质:就是把权限打包给角色(角色拥有一组权限),分配给用户(用户拥有多个角色)。
最少包括五张表 (用户表、角色表、用户角色表、权限表、角色权限表)
java的安全框架实现
主要有三种方式:
- Shiro:轻量级的安全框架,提供认证、授权、会话管理、密码管理、缓存管理等功能
- Spring Security:功能比Shiro强大,更复杂,权限控制细粒度更高,对OAuth2 支持更好,与Spring 框架无缝集合,使Spring Boot 集成很快捷。
- 自己写:基于过滤器(filter)和AOP来实现,难度大,没必要。
认证入门
安全入门项目
1.添加spring-boot-starter-security依赖
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
2.启动程序,并使用浏览器访问http://localhost:8080/student/query
系统会跳转到登录页面
运行结果说明:
spring Security默认拦截了所有请求,但登录退出不拦截
3.登录系统
使用默认用户user登录系统,密码是随机生成的UUID字符串,可以在控制台(console)上找到。
登录系统,再次访问:
|http://localhost:8080/student/query
http://localhost:8080/teacher/query
http://localhost:8080/admin/query
发现登录后的用户均可以正常访问
4.退出系统
http://localhost:8080/logout
单击Log Out 按钮,成功退出
5.结论
引入spring-boot-starter-security依赖后,项目中除登录退出外所有资源都会被保护起来
认证(登录)用户可以访问所有资源,不经过认证用户任何资源也访问不了。
6.问题
所有资源均已保护,但是用户只用一个,密码是随机的,只能在开发环境使用
使用配置文件配置用户名和密码
1.添加spring security 配置信息
spring:
security:
user:
name: admin
password: 888888
2.启动运行并使用浏览器测试
发现使用配置文件中的用户名和密码可以正常访问。
配置文件中配置用户后,默认的user用户就没有了。
示例说明:可以通过配置文件配置用户和密码,解决了使用随机生成密码的问题。
查看源码可以看到没有使用配置文件配置用户名和密码前 默认用户名为user,密码为控制台使用UUID随机生成的字符串。
3. 问题
Spring Security配置文件中默认配置用户是单一的用户,大部分系统都有多个用户,多个用户如何配置?
基于内存的多用户管理
1.新建配置类
com.powernode.config包下新建配置类MySecurityUserConfig,如下:
@Configuration
public class MySecurityUserConfig {
@Bean
public UserDetailsService userDetailService() {
// 使用org.springframework.security.core.userdetails.User类来定义用户
//定义两个用户
UserDetails user1 = User._builder_().username("eric").password("123456").roles("student").build();
UserDetails user2 = User._builder_().username("thomas").password("123456").roles("teacher").build();
//创建两个用户
InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
userDetailsManager.createUser(user1);
userDetailsManager.createUser(user2);
return userDetailsManager;
}
}
2.启动程序测试
登录页面输入用户名(thomas)和密码(123456),然后单击登录后,控制台报错,如下:
报错的原因如下:
这个是因为spring Sercurity强制要使用密码加密,当然我们也可以不加密,但是官方要求是不管你是否加密,都必须配置一个密码编码(加密)器
3.添加密码加密器bean但是不对密码加密
在MySecurityUserConfig类中加入以下bean
/*
*从 Spring5 开始,强制要求密码要加密
*如果非不想加密,可以使用一个过期的 PasswordEncoder 的实例 NoOpPasswordEncoder,
*但是不建议这么做,毕竟不安全。
*@return
*/
@Bean
public PasswordEncoder passwordEncoder(){
//不对密码进行加密,使用明文
return NoOpPasswordEncoder._getInstance_();
}
重启程序再次使用thomas/123456登录测试,可以登录正常访问了。
使用admin/888888登录,登录不成功,说明:我们只要添加了安全配置类,那么我们在****yml里面的配置就失效了
此处可以查看一下NoOpPasswordEncoder源码,再看一下单例模式,加密和密码对比方法
英文小提示:
明文:plaintext
密文:ciphertext
问题:
- 密码为什么要加密?加密的方式有哪些? 涉及到密码加密问题
- NoOpPasswordEncoder此类已经过期,而且还没有加密,如何解决?下章解决
- 以学生身份登录,发现不但可以访问学生的页面,还可以访问教师的页面和管理员的页面,如何解决? 权限问题,后面解决
- 如果要动态的创建用户,或者修改密码等(不是把用户名和密码写死到代码中),怎么办? 认证信息要存储到数据库中。
密码处理
为什么要加密?
csdn 密码泄露事件
泄露事件经过:https://www.williamlong.info/archives/2933.html
泄露数据分析:https://blog.csdn.net/crazyhacking/article/details/10443849
加密方案
密码加密一般使用散列函数,又称散列算法,哈希函数,这些函数都是单向函数(从明文到密文,反之不行)
常用的散列算法有MD5和SHA
Spring Security提供多种密码加密方案,基本上都实现了PasswordEncoder接口,官方推荐使用BCryptPasswordEncoder
BCryptPasswordEncoder类初体验
拷贝springsecurity-04-inmemory工程,重命名为springsecurity-05-password-encode
test/java 下新建包com.powernode.password,在该包下新建测试类PasswordEncoderTest,如下
@Slf4j
public class PasswordEncoderTest {
@Test
@DisplayName("测试加密类BCryptPasswordEncoder")
void testPassword(){
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
//加密(明文到密文)
String encode1 = bCryptPasswordEncoder.encode("123456");
_log_.info("encode1:"+encode1);
String encode2 = bCryptPasswordEncoder.encode("123456");
_log_.info("encode2:"+encode2);
String encode3 = bCryptPasswordEncoder.encode("123456");
_log_.info("encode3:"+encode3);
//匹配方法,判断明文经过加密后是否和密文一样
boolean result1 = bCryptPasswordEncoder.matches("123456", encode1);
boolean result2 = bCryptPasswordEncoder.matches("123456", encode1);
boolean result3 = bCryptPasswordEncoder.matches("123456", encode1);
_log_.info(result1+":"+result2+":"+result3);
_assertTrue_(result1);//断言,这里表示结果的期望值为True,如果运行后的结果是false则会报错
_assertTrue_(result2);
_assertTrue_(result3);
}
}
查看控制台发现特点是:**相同的字符串加密之后的结果都不一样,但是比较的时候是一样的,因为加了盐(**salt)了。
上面简单看下即可
小提示:
Ø 开发代码时不允许使用main方法测试,而是使用单元测试来测试
Ø 代码中一般不允许使用System.out.println 直接输出,而是使用日志输出
Ø 单元测试尽量使用断言,而不是使用System.out.println输出
使用加密器并且加密
修改MySecurityUserConfig类中的加密器bean
@Bean
public PasswordEncoder passwordEncoder(){
//使用加密算法对密码进行加密
return new BCryptPasswordEncoder();
}
启动程序测试,发现不能正常登录
原因是输入的密码是进行加密了,但是系统中定义的用户密码没有加密
将系统定义的用户密码修改成密文,如下
@Configuration
public class MySecurityUserConfig {
@Bean
public UserDetailsService userDetailService() {
// 使用org.springframework.security.core.userdetails.User类来定义用户
//定义两个用户
UserDetails user1 = User._builder_()
.username("eric")
.password(passwordEncoder().encode("123456"))
.roles("student")
.build();
UserDetails user2 = User._builder_()
.username("thomas")
.password(passwordEncoder().encode("123456"))
.roles("teacher")
.build();
//创建两个用户
InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
userDetailsManager.createUser(user1);
userDetailsManager.createUser(user2);
return userDetailsManager;
}
/*
* 从 Spring5 开始,强制要求密码要加密
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
//使用加密算法对密码进行加密
return new BCryptPasswordEncoder();
}
}
重启程序,再次测试即可,发现登录和访问没问题了
查看当前登录用户信息及配置用户权限
获取当前登录用户信息
新建一个controller设置三种方式来获取登陆用户信息
@RestController
public class CurrentLoginUserInfoController {
_/**
* 从当前请求对象中获取
*/
_@GetMapping("/getLoginUserInfo")
public Principal getLoginUserInfo(Principal principle){
return principle;
}
_/**
*从当前请求对象中获取
*/
_@GetMapping("/getLoginUserInfo1")
public Authentication getLoginUserInfo1(Authentication authentication){
return authentication;
}
_/**
* 从安全应用上下文(SecurityContextHolder)获取安全应用上下文(SecurityContext),从安全应用上下文中获取认证信息
* **@return
***/
_@GetMapping("/getLoginUserInfo2")
public Authentication getLoginUserInfo(){
Authentication authentication = SecurityContextHolder._getContext_().getAuthentication();
return authentication;
}
}
注意Authentication接口继承自 Principal
重启程序,访问
http://localhost:8080/getLoginUserInfo
http://localhost:8080/getLoginUserInfo1
http://localhost:8080/getLoginUserInfo2
运行结果
{
“authorities”: [{
“authority”: “ROLE_teacher”
}],
“details”: {
“remoteAddress”: “0:0:0:0:0:0:0:1”,
“sessionId”: “34E452050095348E6306CF95B2025CD9”
},
“authenticated”: true,
“principal”: {
“password”: null,
“username”: “thomas”,
“authorities”: [{
“authority”: “ROLE_teacher”
}],
“accountNonExpired”: true,
“accountNonLocked”: true,
“credentialsNonExpired”: true,
“enabled”: true
},
“credentials”: null,
“name”: “thomas” }
Principal 定义认证的而用户,如果用户使用用户名和密码方式登录,principal通常就是一个UserDetails(后面再说)
Credentials:登录凭证,一般就是指密码。当用户登录成功之后,登录凭证会被自动擦除,以防泄露。
authorities:用户被授予的权限信息。
配置用户权限
配置用户权限有两种方式:
配置roles
配置authorities
注意事项:
如果给一个用户同时配置roles和authorities,哪个写在后面哪个起作用
配置roles时,权限名会加上ROLE_。
修改WebSecurityConfig代码中的
// 注意 1 哪个写在后面哪个起作用 2 角色变成权限后会加一个ROLE_前缀,比如ROLE_teacher
// UserDetails user2 = User.builder()
// .username("thomas")
// .password(passwordEncoder().encode("123456"))
// .authorities("teacher:add","teacher:update")
// .roles("teacher")
// .build();
UserDetails user2 = User._builder_()
.username("thomas")
.password(passwordEncoder().encode("123456"))
.authorities("teacher:add","teacher:update")//设置权限
.roles("teacher")//设置权限
.build();
重启程序使用thomas登录,然后查看用户认证信息
http://localhost:8080/getLoginUserInfo
结果:
可以看到authorities的情况。
从设计层面讲,角色和权限是两个完全不同的东西
从代码层面来讲,角色和权限并没有太大区别,特别是在Spring Security中
授权(对URL进行授权)
上面讲的实现了认证功能,但是受保护的资源是默认的,默认所有认证(登录)用户均可以访问所有资源,不能根据实际情况进行角色管理,要实现授权功能,需重写WebSecurityConfigureAdapter 中的一个configure方法
复制springsecurity-06-loginuser-info 工程,然后改名为springsecurity-07-url
新建WebSecurityConfig类,重写configure(HttpSecurity http)方法
WebSecurityConfig 完整代码如下:
@Configuration
@Slf4j
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
//角色student或者teacher都可以访问/student/** 这样的url
.mvcMatchers("/student/*").hasAnyRole("student:add", "teacher")
// 角色teacher 可以访问teacher/**
.mvcMatchers("/teacher/**").hasRole("teacher")
//权限admin:query 可以访问/admin**
//.mvcMatchers("/admin/**").hasAuthority("admin:query")
//角色teacher 或者权限admin:query 觉可以访问admin/**
.mvcMatchers("/admin/**").access("hasRole('teacher') or hasAuthority('admin:query')")
.anyRequest().authenticated();//任何请求均需要认证,没有配置的路径是只要登陆成功就可以访问
//使用表单登录
http.formLogin();
}
}
使用admin登录,访问
http://localhost:8080/teacher/query
http://localhost:8080/student/query
http://localhost:8080/admin/query
分别查看效果,实现权限控制
上面是对URL资源进行控制,就是哪些权限可以访问哪些URL。
授权(方法级别的权限控制)
上面学习的认证与授权都是基于URL的,我们也可以通过注解灵活的配置方法安全,我们先通过@EnableGlobalMethodSecurity开启基于注解的安全配置。
新建模块springsecurity-08-method
添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
新建启动类并复制 CurrentLoginUserInfoController类
新建启动类Application,学员自行创建
新建service及其实现
com.powernode.service 新建教师接口
public interface TeacherService {
String add();
String update();
String delete();
String query();
}
com.powernode.service.impl 实现接口
@Service
@Slf4j
public class TeacherServiceImpl implements TeacherService {
@Override
public String add() {
_log_.info("添加教师成功");
return "添加教师成功";
}
@Override
public String update() {
_log_.info("修改教师成功");
return "修改教师成功";
}
@Override
public String delete() {
_log_.info("删除教师成功");
return "删除教师成功";
}
@Override
public String query() {
_log_.info("查询教师成功");
return "查询教师成功";
}
}
新建TeacherController
@RestController
@RequestMapping("/teacher")
public class TeacherController {
@Resource
private TeacherService teacherService;
@GetMapping("/query")
public String queryInfo() {
return teacherService.query();
}
@GetMapping("/add")
public String addInfo() {
return teacherService.add();
}
@GetMapping("/update")
public String updateInfo() {
return teacherService.update();
}
@GetMapping("/delete")
public String deleteInfo() {
return teacherService.delete();
}
}
新建安全配置类
com.powernode.config下新建用户配置类
@Configuration
public class MySecurityUserConfig {
@Bean
public UserDetailsService userDetailService() {
// 使用org.springframework.security.core.userdetails.User类来定义用户
//定义两个用户
UserDetails user1 = User._builder_()
.username("eric")
.password(passwordEncoder().encode("123456"))
.roles("student")
.build();
UserDetails user2 = User._builder_()
.username("thomas")
.password(passwordEncoder().encode("123456"))
.authorities("teacher:query")
.roles("teacher")
.build();
UserDetails user3 = User._builder_()
.username("admin")
.password(passwordEncoder().encode("123456"))
.authorities("teacher:add", "teacher:update")
.roles("teacher")
.build();
//创建两个用户
InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
userDetailsManager.createUser(user1);
userDetailsManager.createUser(user2);
return userDetailsManager;
}
/*
* 从 Spring5 开始,强制要求密码要加密
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
//使用加密算法对密码进行加密
return new BCryptPasswordEncoder();
}
}
新建WebSecurityConfig类
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
//任何访问均需要认证
http.authorizeRequests().anyRequest().authenticated();
http.formLogin(); //使用表单登陆方式
}//这意味着用户访问任何URL时,都会被重定向到登录页面,
//要求用户输入用户名和密码进行认证。一旦认证成功,用户即可访问受保护的资源。
}
启动程序并访问
访问以下地址
| http://localhost:8080/teacher/add
http://localhost:8080/teacher/update
http://localhost:8080/teacher/delete
http://localhost:8080/teacher/query
通过admin或thomas登录均可以访问所有资源
修改安全配置类WebSecurityConfig
加上启用全局方法安全注解
@EnableGlobalMethodSecurity(prePostEnabled = true)
修改后,完整代码如下:
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
//任何访问均需要认证
http.authorizeRequests().anyRequest().authenticated();
//使用表单登录
http.formLogin();
}
}
修改TeacherServiceImpl
在每个方法上加上前置授权注解:@PreAuthorize
完整代码如下:
@Service
@Slf4j
public class TeacherServiceImpl implements TeacherService {
@Override
@PreAuthorize("hasAuthority('teacher:add') OR hasRole('teacher')")//预授权
public String add() {
_log_.info("添加教师成功");
return "添加教师成功";
}
@Override
@PreAuthorize("hasAuthority('teacher:update')")
public String update() {
_log_.info("修改教师成功");
return "修改教师成功";
}
@Override
@PreAuthorize("hasAuthority('teacher:delete')")
public String delete() {
_log_.info("删除教师成功");
return "删除教师成功";
}
@Override
@PreAuthorize("hasRole('teacher')")
public String query() {
_log_.info("查询教师成功");
return "查询教师成功";
}
}
启动并运行
运行程序分别使用admin和teacher登录,可以查看不同效果
http://localhost:8080/teacher/add
http://localhost:8080/teacher/update
http://localhost:8080/teacher/delete
http://localhost:8080/teacher/query
发现thomas可以访问添加和查询,别的不能访问,amdin可以访问添加和更新,别的不能访问。
代码说明:
Ø EnableGlobalMethodSecurity注解的属性prePostEnabled = true 解锁@PreAuthorize 和@PostAuthorize注解,@PreAuthorize 在方法执行前进行验证,@PostAuthorize 在方法执行后进行验证
Ø EnableGlobalMethodSecurity的securedEnabled = true 解锁@Secured注解,@Secured和@PreAuthorize用法基本一样 @Secured对应的角色必须要有ROLE_前缀
SpringSecurity 返回json
前后端分离成为企业应用开发中的主流,前后端分离通过json进行交互,登录成功和失败后不用页面跳转,而是一段json提示
新建模块springsecurity-09-json
添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
新建三个controller和获取登录用户信息的controller
新建启动类
com.powernode下新建Application类,学员自行创建
创建统一响应类HttpResult
在com.powernode.vo中创建该类
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class HttpResult {
private Integer code;
private String msg;
private Object data;
public HttpResult(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
}
创建登录成功处理器
com.powernode.config 包下创建
@Component
public class MyAutheticationSuccessHandle implements AuthenticationSuccessHandler {
@Resource
private ObjectMapper objectMapper;//进行序列化
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=utf-8");
HttpResult httpResult = HttpResult.builder().code(200).msg("登录成功").build();
String str = objectMapper.writeValueAsString(httpResult);
response.getWriter().write(str);
response.getWriter().flush();
}
}
创建登录失败处理器
/**
* 登陆失败的处理器
*/
@Component
@Slf4j
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Resource
private ObjectMapper objectMapper;
/**
* **@param **request 当前的请求对象
* **@param **response 当前的响应对象
* **@param **exception 失败的原因的异常
* **@throws **IOException
* **@throws **ServletException
*/
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
System._err_.println("登陆失败");
//设置响应编码
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=utf-8");
//返回JSON出去
HttpResult result=HttpResult.builder().code(-1).msg("登录失败").build();
if(exception instanceof BadCredentialsException){
result.setData("密码不正确");
}else if(exception instanceof DisabledException){
result.setData("账号被禁用");
}else if(exception instanceof UsernameNotFoundException){
result.setData("用户名不存在");
}else if(exception instanceof CredentialsExpiredException){
result.setData("密码已过期");
}else if(exception instanceof AccountExpiredException){
result.setData("账号已过期");
}else if(exception instanceof LockedException){
result.setData("账号被锁定");
}else{
result.setData("未知异常");
}
//把result转成JSON
String json = objectMapper.writeValueAsString(result);
//响应出去
PrintWriter out = response.getWriter();
out.write(json);
out.flush();
}
}
创键无权限处理器
/**
* 无权限的处理器
*/
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
//声明一个把对象转成JSON的对象
@Resource
private ObjectMapper objectMapper;
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
//设置响应编码
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=utf-8");
//创建响应对象
HttpResult result= HttpResult.builder()
.code(-1)
.msg("用户没有访问权限")
.build(); //把result转成JSON
String json = objectMapper.writeValueAsString(result);
//响应json出去
PrintWriter out = response.getWriter();
out.write(json);
out.flush();
}
}
创建登出(退出)处理器
/**
* 退出成功的处理器
*/
@Component
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
//声明一个把对象转成JSON的对象@Resource
private ObjectMapper objectMapper;
/**
*
* **@param **request
* **@param **response
* **@param **authentication 当前退出的用户对象
* **@throws **IOException
* **@throws **ServletException
*/
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
System._out_.println("退出成功");
//设置响应编码
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=utf-8");
//返回JSON出去
HttpResult result= HttpResult.builder().code(200).msg("退出成功").build();
//把result转成JSON
String json = objectMapper.writeValueAsString(result);
//响应出去
PrintWriter out = response.getWriter();
out.write(json);
out.flush();
}
}
创建用户配置类
@Configuration
public class MySecurityUserConfig {
@Bean
public UserDetailsService userDetailService() {
//使用org.springframework.security.core.userdetails.User类来定义用户
//定义用户
UserDetails user1 = User._builder_()
.username("eric")
.password(passwordEncoder().encode("123456"))
.roles("student")
.build();
UserDetails user2 = User._builder_()
.username("thomas")
.password(passwordEncoder().encode("123456"))
.roles("teacher")
.build();
UserDetails user3 = User._builder_()
.username("admin")
.password(passwordEncoder().encode("123456"))
.roles("admin")
.build();
//创建用户
InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
userDetailsManager.createUser(user1);
userDetailsManager.createUser(user2);
userDetailsManager.createUser(user3);
return userDetailsManager;
}
/*
* 从 Spring5 开始,强制要求密码要加密
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
//使用加密算法对密码进行加密
return new BCryptPasswordEncoder();
}
}
安全配置类WebSecurityConfig
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
// 注入登陆成功的处理器
@Autowired
private MyAutheticationSuccessHandle successHandler;
// 注入登陆失败的处理器
@Autowired
private MyAuthenticationFailureHandler failureHandler;
// 注入没有权限的处理器
@Autowired
private MyAccessDeniedHandler accessDeniedHandler;
// 注入退出成功的处理器
@Autowired
private MyLogoutSuccessHandler logoutSuccessHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
//配置拒绝访问处理器
http.exceptionHandling().accessDeniedHandler(accessDeniedHandler);
//配置登录成功处理器
http.formLogin().successHandler(successHandler);
//配置登录失败处理器
http.formLogin()failureHandler(failureHandler);
//配置退出成功处理器
http.logout().logoutSuccessHandler(logoutSuccessHandler);
http.authorizeRequests().mvcMatchers("/teacher/**").hasRole("teacher").anyRequest().authenticated();
}
}
启动程序
访问测试
可以使用admin用户实验登录失败、登录成功、退出和访问http://localhost:8080/teacher/query 查看无权访问的效果
使用自定义UserDetailsService实现获取用户认证信息
新建子模块springsecurity-10-userdetailservice
添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
新建启动类
com.powernode包下新建启动类Application,学员自行创建
新建三个controller
可以直接拷贝过来
新建获取登录用户认证信息的controller
@RestController
public class CurrentLoginUserInfoController {
_/**
* 从当前请求对象中获取
*/
_@GetMapping("/getLoginUserInfo")
public Principal getLoginUserInfo(Principal principle){
return principle;
}
_/**
*从当前请求对象中获取
*/
_@GetMapping("/getLoginUserInfo1")
public Authentication getLoginUserInfo1(Authentication authentication){
return authentication;
}
_/**
* 从安全应用上下文(SecurityContextHolder)获取安全应用上下文(SecurityContext),从安全应用上下文中获取认证信息
* **@return
***/
_@GetMapping("/getLoginUserInfo2")
public Authentication getLoginUserInfo(){
Authentication authentication = SecurityContextHolder._getContext_().getAuthentication();
return authentication;
}
}
新建用户信息类
com.powernode.vo包下新建SecurityUser 类,该类实现接口UserDetails接口(后面会和数据库交互获取用户)
public class SecurityUser implements UserDetails {
@Override//这里负责分配权限
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
//用户密码使用密文 return new BCryptPasswordEncoder().encode("123456");
}
@Override//这里负责定义用户,现在已经写死了,后面会从数据库中取用户
public String getUsername() {//定义用户名
return "thomas";
}
@Override
public boolean isAccountNonExpired() {//账号是否未过期,返回true 未过期
return true;
}
@Override
public boolean isAccountNonLocked() {//账号是否未锁定,返回true 未锁定
return true;
}
@Override
public boolean isCredentialsNonExpired() {//凭据(凭证),目前可以理解成密码,是否未过期,返回true 未过期
return true;
}
@Override
public boolean isEnabled() {//账号是否可以,返回true可用
return true;
}
}
代码说明:
用户实体类需要实现UserDetails接口,并实现该接口中的7个方法, UserDetails 接口的7个方法如下图:
新建类实现UserDetailService接口
com.powernode.service.impl 包下新建UserServiceImpl 实现UserDetailService
框架提供一个UserDetailsService接口用来加载用户信息。
UserDetailsService里面只有一个方法,作用就是通过username查询用户的信息。
@Service
public class UserServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SecurityUser securityUser= new SecurityUser();
if(username==null || !username.equals(securityUser.getUsername())){
throw new UsernameNotFoundException("该用户不存在");
}
return securityUser;
}
}
新建安全配置类
com.powernode.config下新建WebSecurityConfig类,配置密码编码器
@Configuration
@Slf4j
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
启动程序,并使用浏览器访问程序:http://localhost:8080/student/query
发现需要登录,使用thomas/123456 登录后,即可正常访问。
访问:http://localhost:8080/getLoginUserInfo
发现该用户并没有权限信息,因为SecurityUser类中也没有分配权限
配置用户权限信息
修改SecurityUser类中的getAuthorities 方法
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
GrantedAuthority g1=()->"student:query"; //使用lambda表达式创建接口实现类,而不是使用匿名内部类来实现接口
//GrantedAuthority g1=new SimpleGrantedAuthority("student:query"); // 使用子类创建对象
List<GrantedAuthority> grantedAuthorityList=new ArrayList<>();
grantedAuthorityList.add(g1);
return grantedAuthorityList;
}
修改要访问controller中的方法需要哪些权限
修改WebSecurityConfig,添加全局方法拦截注解@EnableGlobalMethodSecurity(prePostEnabled = true)
注意可以去掉:@Configuration注解了
修改StudentController
添加 @PreAuthorize(“hasAuthority(‘student:query’)”) 注解修改后如下:
@RestController
@RequestMapping("/student")
public class StudentController {
@GetMapping("/query")
@PreAuthorize("hasAuthority('student:query')")
public String queryInfo(HttpServletRequest request){
return "I am a student,My name is Eric";
}
}
修改TeacherController
添加 @PreAuthorize(“hasAuthority(teacher:query’)”) 注解修改后如下:
@RestController
@RequestMapping("/teacher")
public class TeacherController {
@GetMapping("/query")
@PreAuthorize("hasAuthority('teacher:query')")
public String queryInfo(){
return "I am a teacher,My name is Thomas";
}
}
启动测试,使用thomas/123456 登录系统,发现可以访问student/query,不可以访问teacher/query
再次访问:http://localhost:8080/getLoginUserInfo 查看用户信息
为什么讲这个示例?
是为了使用数据库存储用户角色权限信息做准备,只要从数据库中取出数据存储到实现UserDetails 的接口的类中即可,比如SecurityUser 中即可。
基于数据库的认证
创建数据库security_study和表
创建数据库security_study
导入数据库脚本security_study.sql
创建模块springsecurity-11-database-authentication
添加依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.13</version>
</parent>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<java.version>8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</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-security</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
配置数据源和mybatis
新建配置文件application.yml并配置数据源和mybatis
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/security_study?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: root
mybatis:
type-aliases-package: com.powernode.entity//定义了实体类的包名,MyBatis会自动扫描该包下的实体类
configuration:
map-underscore-to-camel-case: true//驼峰式命名
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl//设置日志输出的实现类
mapper-locations: classpath:mapper/*.xml
//表示映射文件位于classpath下的mapper目录中,所有以.xml扩展名结尾的文件都会被作为映射文件识别。
新建各个包
新建用户实体类
com.powernode.entity 包下新建用户实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class SysUser implements Serializable {
private Integer userId;
private String username;
private String password;
private String sex;
private String address;
private Integer enabled;
private Integer accountNoExpired;
private Integer credentialsNoExpired;
private Integer accountNoLocked;
}
新建用户mapper和映射文件
com.powernode.dao下新建
public interface SysUserDao {
/**
* 根据用户名获取用户信息
* **@param **username
* **@return
***/
SysUser getByUserName(@Param("username") String username);
}
mapper下新建映射文件SysUserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.powernode.dao.SysUserDao">
<select id="getByUserName" resultType="sysUser">
select user_id,username,password,sex,address,enabled,account_no_expired,credentials_no_expired,account_no_locked
from sys_user where username=#{username}
</select>
</mapper>
新建启动类
com.powernode包下新建启动类
@SpringBootApplication
@MapperScan("com.powernode.dao")
public class Application {
public static void main(String[] args) {
SpringApplication._run_(Application.class,args);
}
}
单元测试
测试dao
@SpringBootTest
class SysUserDaoTest {
@Resource
private SysUserDao sysUserDao;
@Test
void getByUserName() {
SysUser sysUser = sysUserDao.getByUserName("obama");
_assertNotNull_(sysUser);
}
}
注意单元测试要测试哪些:dao–service-controller,实体类一般不需要测试
新建安全用户类
com.powernode.vo包下新建类
public class SecurityUser implements UserDetails {
private final SysUser sysUser;
public SecurityUser(SysUser sysUser) {
this.sysUser=sysUser;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
String userPassword=this.sysUser.getPassword();
//注意清除密码
this.sysUser.setPassword(null);
return userPassword;
}
@Override
public String getUsername() {
return sysUser.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return sysUser.getAccountNoExpired().equals(1);
}
@Override
public boolean isAccountNonLocked() {
return sysUser.getAccountNoLocked().equals(1);
}
@Override
public boolean isCredentialsNonExpired() {
return sysUser.getCredentialsNoExpired().equals(1);
}
@Override
public boolean isEnabled() {
return sysUser.getEnabled().equals(1);
}
}
新建UserServiceImpl 实现UserDetailService接口
@Service
public class UserServiceImpl implements UserDetailsService {
@Resource
private SysUserDao sysUserDao;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser sysUser = sysUserDao.getByUserName(username);
if(null==sysUser){
throw new UsernameNotFoundException("账号不存在");
}
return new SecurityUser(sysUser);
}
}
新建service单元测试
@SpringBootTest
class UserServiceImplTest {
@Resource
private UserServiceImpl userService;
@Test
void loadUserByUsername() {
UserDetails userDetails = userService.loadUserByUsername("obama");
assertNotNull_(userDetails);
}
}
新建两个控制器
@RestController
@Slf4j
@RequestMapping("/student")
public class StudentController {
@GetMapping("/query")
public String queryInfo(){
return "query student";
}
@GetMapping("/add")
public String addInfo(){
return "add student!";
}
@GetMapping("/update")
public String updateInfo(){
return "update student";
}
@GetMapping("/delete")
public String deleteInfo(){
return "delete student!";
}
@GetMapping("/export")
public String exportInfo(){
return "export student!";
}
}
@RestController
@Slf4j
@RequestMapping("/teacher")
public class TeacherController {
@GetMapping("/query")
@PreAuthorize("hasAuthority('teacher:query')")
public String queryInfo(){
return "I am a teacher!";
}
}
新建获取登录用户认证信息的controller
新建web安全配置类
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Slf4j
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean//密码加密器
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated();//所有请求必须登录认证
http.formLogin();//表单登录,登录页面
}
}
启动并进行各种测试
使用thomas和obama分别登录测试,发现student/query等能访问(不需要访问权限),teacher/query 不能访问(需要访问权限),原因
http://localhost:8080/getLoginUserInfo
发现用户没有权限,但是/teacher/query 需要访问权限,所以/teacher/query 无法访问
基于数据库的方法授权
新建模块
复制springsecurity-11-database-authentication 改名为springsecurity-12-database-authorization-method
注意这个工程已经有认证功能了。下面咱们看下如何设置用户的权限
新建菜单(权限)实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class SysMenu implements Serializable {
private Integer id;
private Integer pid;
private Integer type;
private String name;
private String code;
}
新建权限mapper和映射文件
public interface SysMenuDao {
//根据userid获取到用户所拥有的权限
List<String> queryPermissionByUserId(@Param("userId") Integer userId);
}
映射文件SysMenuMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.powernode.dao.SysMenuDao">
<select id="queryPermissionByUserId" resultType="string">
SELECT distinct sm.`code` FROM `sys_role_user` sru inner join sys_role_menu srm
on sru.rid=srm.rid inner join sys_menu sm on srm.mid=sm.id where sru.uid=#{userId} and sm.delete_flag=0
</select>
</mapper>
权限dao的单元测试
@SpringBootTest
class SysMenuDaoTest {
@Resource
private SysMenuDao sysMenuDao;
@Test
void queryPermissionByUserId() {
List<String> menuList = sysMenuDao.queryPermissionByUserId(1);
_assertTrue_(!menuList.isEmpty());
}
}
新建SysMenuService与其SysMenuServiceImpl
修改SecurityUser实体类
加入一个属性
private List simpleGrantedAuthorities;//用于存储权限的List
修改方法getAuthorities
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return simpleGrantedAuthorities;
}
添加一个set方法
public void setSimpleGrantedAuthorities(List<SimpleGrantedAuthority> simpleGrantedAuthorities) {
this.simpleGrantedAuthorities = simpleGrantedAuthorities;
}
修改UserServiceImpl
增加设置权限的步骤,修改后如下:
@Service
@Slf4j
public class UserServiceImpl implements UserDetailsService {
@Resource
private SysUserDao sysUserDao;
@Resource
private SysMenuDao sysMenuDao;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser sysUser = sysUserDao.getByUserName(username);
if(null==sysUser){
throw new UsernameNotFoundException("账号不存在");
}
//获取到用户的权限列表
List<String> strList=sysMenuDao.queryPermissionByUserId(sysUser.getUserId());
//使用stream流来转换// SimpleGrantedAuthority::new 相当于调用构造方法
//这将通过将每个strList中的元素映射为SimpleGrantedAuthority对象,并将结果收集到一个新的List中。
List<SimpleGrantedAuthority> grantedAuthorities=strList.stream().map(SimpleGrantedAuthority::new).collect(_toList_());
SecurityUser securityUser = new SecurityUser(sysUser);
securityUser.setSimpleGrantedAuthorities(grantedAuthorities);
return securityUser;
}
}
启动并进行各种测试
使用thomas和obama分别登录测试,发现已经有权限功能了
SpringSecurity 集成thymeleaf
此项目是在springsecurity-12-database-authorization-method 的基础上进行
复制springsecurity-12-database-authorization-method 并重命名为springsecurity-13-thymeleaf
添加thymeleaf依赖
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
修改application.yml
加入thymeleaf的配置
thymeleaf:
cache: flase #开发时可以不使用缓存
check-template: true #是否检查模版
idea 添加thymeleaf模板
【File】—》【Settings…】
新建LoginController
@Controller
@RequestMapping(“/login”)
public class LoginController {
/**
* 跳转到登陆页面
*/
@RequestMapping("/toLogin")
public String toLogin(){
//返回thymeleaf的视图
return "login";
}
创建thymeleaf文件login.html
在templates下面创建login.html,使用模板创建,templates文件在resources下
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>用户登录</title>
</head>
<body>
<h2>登录页面</h2>
<form action="/login/doLogin" method="post">
<table>
<tr>
<td>用户名:</td>
<td><input type="text" name="uname" value="thomas"></td>
</tr>
<tr>
<td>密码:</td>
<td><input type="password" name="pwd"></td>
</tr>
<tr>
<td colspan="2">
<button type="submit">登录</button>
</td>
</tr>
</table>
</form>
</body>
</html>
修改安全配置文件WebSecurityConfig
@EnableGlobalMethodSecurity(prePostEnabled = true)
//@Configuration
@Slf4j
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
配置路径拦截 的url的匹配规则,所有的请求都需要认证
http.authorizeRequests().anyRequest().authenticated();
//设置登陆方式
http.formLogin()//使用用户名和密码的登陆方式
.usernameParameter("uname") //页面表单的用户名的name
.passwordParameter("pwd")//页面表单的密码的name
.loginPage("/login/toLogin") //后端自己定义登陆页面的地址
.loginProcessingUrl("/login/doLogin")//前端页面配置登陆的url
.successForwardUrl("/index/toIndex") //登陆成功跳转的页面(/index/toIndex是后端Controller的一个路径)
.failureForwardUrl("/login/toLogin")//登陆失败跳转的页面
.permitAll(); //放行和登陆有关的url,别忘了写这个
//配置退出方式
http.logout()
.logoutUrl("/logout")//指定注销(退出)URL的路径,即在访问/logout时触发注销操作。
.logoutSuccessUrl("/login/toLogin")//配置退出成功后跳转的页面
.permitAll();//放行和退出有关的url,别忘了写这个
// 由于目前没配置token,先禁用csrf跨站请求攻击 后面可以使用postman工具测试,注意要禁用csrf
http.csrf().disable();
}
创建IndexController
@Controller
@RequestMapping(“/index”)
public class IndexController {
/**
* 登录成功后进入主页
*/
@RequestMapping("/toIndex")
public String toIndex(){
return "main";
}
创建thymeleaf文件main.html
在templates下面创建main.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>系统首页</title>
</head>
<body>
<h1 align="center">系统首页</h1>
<a href="/student/query">查询学生</a>
<br>
<a href="/student/add">添加学生</a>
<br>
<a href="/student/update">更新学生</a>
<br>
<a href="/student/delete">删除学生</a>
<br>
<a href="/student/export">导出学生</a>
<br>
<br><br><br>
<h2><a href="/logout">退出</a></h2>
<br>
</body>
</html>
修改Studentcontroller
@Controller
@Slf4j
@RequestMapping("/student")
public class StudentController {
@GetMapping("/query")
@PreAuthorize("hasAuthority('student:query')")
public String queryInfo(){
return "user/query";
}
@GetMapping("/add")
@PreAuthorize("hasAuthority('student:add')")
public String addInfo(){
return "user/add";
}
@GetMapping("/update")
@PreAuthorize("hasAuthority('student:update')")
public String updateInfo(){
return "user/update";
}
@GetMapping("/delete")
@PreAuthorize("hasAuthority('student:delete')")
public String deleteInfo(){
return "user/delete";
}
@GetMapping("/export")
@PreAuthorize("hasAuthority('student:export')")
public String exportInfo(){
return "/user/export";
}
}
在templates/user下面创建学生管理的各个页面
创建export.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>系统首页-学生管理</title>
</head>
<body>
<h1 align="center">系统首页-学生管理-导出</h1>
<a href="/index/toIndex">返回</a>
<br>
</body>
</html>
创建query.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>系统首页-学生管理</title>
</head>
<body>
<h1 align="center">系统首页-学生管理-查询</h1>
<a href="/index/toIndex">返回</a>
<br>
</body>
</html>
创建add.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>系统首页-学生管理</title>
</head>
<body>
<h1 align="center">系统首页-学生管理-新增</h1>
<a href="/index/toIndex">返回</a>
<br>
</body>
</html>
创建update.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>系统首页-学生管理</title>
</head>
<body>
<h1 align="center">系统首页-学生管理-更新</h1>
<a href="/index/toIndex">返回</a>
<br>
</body>
</html>
创建delete.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>系统首页-学生管理</title>
</head>
<body>
<h1 align="center">系统首页-学生管理-删除</h1>
<a href="/index/toIndex">返回</a>
<br>
</body>
</html>
创建403页面
在resources/static/error下面创建403.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>403</title>
</head>
<body>
<h2>403:你没有权限访问此页面</h2>
<a href="/index/toIndex">去首页</a>
</body>
</html>
启动测试
注意:如果出现404问题,一般不出现这个问题
<parent>
<artifactId>spring-boot-starter-parent</artifactId> <groupId>org.springframework.boot</groupId><!--
<version>2.3.12.RELEASE</version>-->
<version>2.6.13</version>
<!--修改sprigboot的版本,然后再修改回去,就好了-->
<relativePath/>
</parent>
当用户没有某权限时,页面不展示该按钮(简单看下即可)
上一讲里面我们创建的项目里面是当用户点击页面上的链接请求到后台之后没有权限会跳转到403,那么如果用户没有权限,对应的按钮就不显示出来,这样岂不是更好吗?
我们接着上一个项目来改造
引入下面的依赖
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
修改main.html即可
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<meta charset="UTF-8">
<title>系统首页</title>
</head>
<body>
<h1 align="center">系统首页</h1>
<a href="/student/query" sec:authorize="hasAuthority('student:query')" >查询用户</a>
<br>
<a href="/student/add" sec:authorize="hasAuthority('student:save')" >添加用户</a>
<br>
<a href="/student/update" sec:authorize="hasAuthority('student:update')" >更新用户</a>
<br>
<a href="/student/delete" sec:authorize="hasAuthority('student:delete')" >删除用户</a>
<br>
<a href="/student/export" sec:authorize="hasAuthority('student:export')" >导出用户</a>
<br>
<br><br><br>
<h2><a href="/logout">退出</a></h2>
<br>
</body>
</html>
重新启动登录后查看效果
springsecurity 集成图片验证码
以前因为我们自己写登陆的方法可以在自己的登陆方法里面去接收页面传过来的code,再和session里面正确的code进行比较 。
概述
上一讲里面我们集成了thymeleaf实现在页面链接的动态判断是否显示,那么在实际开发中,我们会遇到有验证码的功能,那么如何处理呢?
复制上一个工程springsecurity-13-thymeleaf ,修改名字为springsecurity-14-captcha
原理、存在问题、解决思路
Springsecurity的过滤器链
我们知道Spring Security是通过过滤器链来完成了,所以它的解决方案是创建一个过滤器放到Security的过滤器链中,在自定义的过滤器中比较验证码
添加依赖(用于生成验证码)
<!--引入hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.3.9</version>
</dependency>
添加一个获取验证码的接口
@Controller
@Slf4j
public class CaptchaController {
@GetMapping("/code/image")
public void getCaptcha(HttpServletRequest request, HttpServletResponse response) throws IOException {
//创建一个验证码
CircleCaptcha circleCaptcha = CaptchaUtil._createCircleCaptcha_(200, 100, 2, 20);
// 为什么要重构?重构的快捷键是啥?
String captchaCode=circleCaptcha.getCode();
_log_.info("生成的验证码为:{}",captchaCode);
//将验证码放到session中
request.getSession().setAttribute("LOGIN_CAPTCHA_CODE",captchaCode);
//将图片放到响应流,参数分别是图片,图片格式,响应流
ImageIO._write_(circleCaptcha.getImage(),"JPEG",response.getOutputStream());
}
}
创建验证码过滤器
创建一个filiter包再创建验证码过滤器放到认证(用户名密码)过滤器之前
@Component
@Slf4j
public class ValidateCodeFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// TODO 判断路径 是否是/login/doLogin
String requestURI = request.getRequestURI();
if(!requestURI.equals("/login/doLogin")){// 不是登录请求,直接放行
doFilter(request,response,filterChain); //直接下一个过滤器
return;
}
//校验验证码
validateCode(request,response,filterChain);
}
private void validateCode(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException {
//1 从前端获取用户输入的验证码
String enterCode = request.getParameter("code");
//2 从session中获取验证码
String captchaCodeInSession = (String) request.getSession().getAttribute("CAPTCHA_CODE");
request.getSession().removeAttribute("captcha_code_error"); //清除提示信息
if(StringUtils.isEmpty(enterCode)){
request.getSession().setAttribute("captcha_code_error","请输入验证码");
response.sendRedirect("/toLogin");
return;
}
if(StringUtils.isEmpty(captchaCodeInSession)){
request.getSession().setAttribute("captcha_code_error","验证码错误");
response.sendRedirect("/toLogin");
return;
}
//3 判断二者是否相等
if(!enterCode.equalsIgnoreCase(captchaCodeInSession)){
request.getSession().setAttribute("captcha_code_error","验证码输入错误");
response.sendRedirect("/toLogin");
return;
}
request.getSession().removeAttribute("CAPTCHA_CODE"); //删除session中的验证码
//如果程序执行到这里,说明验证码正确,放行
this.doFilter(request,response,filterChain);
}
}
修改WebSecurityConfig(重点)
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Slf4j
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private ValidateCodeFilter validateCodeFilter;
@Override
/**
* Security的http请求配置
* **@param **http
* **@throws **Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
//设置登陆方式
http.formLogin()//使用用户名和密码的登陆方式
.usernameParameter("uname") //页面表单的用户名的name
.passwordParameter("pwd")//页面表单的密码的name
.loginPage("/login/toLogin") //自己定义登陆页面的地址
.loginProcessingUrl("/login/doLogin")//配置登陆的url
.successForwardUrl("/index/toIndex") //登陆成功跳转的页面
.failureForwardUrl("/login/toLogin")//登陆失败跳转的页面
.permitAll(); // 这个不要忘了
//配置退出方式
http.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/login/toLogin")
.permitAll();
//配置路径拦截 的url的匹配规则 ,放行请求获取验证码的路径
http.authorizeRequests().antMatchers("/code/image").permitAll()
//任何路径要求必须认证之后才能访问
.anyRequest().authenticated();
// 禁用csrf跨站请求,注意不要写错了,因为前端页面没有传token,所以要禁用
http.csrf().disable();
**// 配置登录之前添加一个验证码的过滤器** http.addFilterBefore(validateCodeFilter,UsernamePasswordAuthenticationFilter.class);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
修改login.html
添加验证码表单元素和图片
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>用户登录</title>
</head>
<body>
<h2>登录页面</h2>
<form action="/login/doLogin" method="post">
<table>
<tr>
<td>用户名:</td>
<td><input type="text" name="uname" value="thomas"></td>
</tr>
<tr>
<td>密码:</td>
<td><input type="password" name="pwd"></td>
</tr>
<tr>
<td>验证码:</td>
<td><input type="text" name="code"> <img src="/code/image" style="height:33px;cursor:pointer;" onclick="this.src=this.src">
<span th:text="${session.captchaCodeErrorMsg}" style="color: #FF0000;" >username</span>
</td>
</tr>
<tr>
<td colspan="2">
<button type="submit">登录</button>
</td>
</tr>
</table>
</form>
</body>
</html>
测试登录
故意输入错误验证码
使用debug模式,查看一下自定义过滤器执行流程
Base64 和JWT学习
见《base64及jwt学习文档.doc》
JWT+Spring Security+redis+mysql 实现认证
新建工程
复制工程springsecurity-12-database-authorization-method,改名字为
springsecurity-16-jwt-authentication
注意这个工程已经有认证功能和基于方法授权的功能了。
下面咱们看下如何设置使用jwt进行认证登录。
添加jwt依赖
<!-- 添加jwt的依赖 -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.11.0</version>
</dependency>
application.yml 中配置密钥
jwt:
secretKey: thomas12345678
jwt功能类
com.powernode.util包下创建
@Component
@Slf4j
public class JwtUtils {
//算法密钥
@Value("${jwt.secretKey}")
private String jwtSecretKey;
/**
* 创建jwt
*
* **@param **userInfo 用户信息
* **@param **authList 用户权限列表
* **@return **返回jwt(JSON WEB TOKEN)
*/
public String createToken(String userInfo, List<String> authList) {
//创建时间
Date currentTime = new Date();
//过期时间,5分钟后过期
Date expireTime = new Date(currentTime.getTime() + (1000 * 60 * 5));
//jwt 的header信息
Map<String, Object> headerClaims = new HashMap<>();
headerClaims.put("type", "JWT");
headerClaims.put("alg", "HS256");
//创建jwt
return JWT._create_()
.withHeader(headerClaims) // 头部
.withIssuedAt(currentTime) //已注册声明:签发日期,发行日期
.withExpiresAt(expireTime) //已注册声明 过期时间
.withIssuer("thomas") //已注册声明,签发人
.withClaim("userInfo", userInfo) //私有声明,可以自己定义,放入登录用户信息
.withClaim("authList", authList) //私有声明,可以自定义
.sign(Algorithm._HMAC256_(jwtSecretKey)); // 签名,使用HS256算法签名,并使用密钥
// HS256是一种对称算法,这意味着只有一个密钥,在双方之间共享。 使用相同的密钥生成签名并对其进行验证。 应特别注意钥匙是否保密。
}
/**
* 验证jwt的签名,简称验签
*
* **@param **token 需要验签的jwt
* **@return **验签结果
*/
public boolean verifyToken(String token) {
//获取验签类对象
JWTVerifier jwtVerifier = JWT._require_(Algorithm._HMAC256_(jwtSecretKey)).build();
try {
//验签,如果不报错,则说明jwt是合法的,而且也没有过期
DecodedJWT decodedJWT = jwtVerifier.verify(token);
return true;
} catch (JWTVerificationException e) {
//如果报错说明jwt 为非法的,或者已过期(已过期也属于非法的)
_log_.error("验签失败:{}", token);
e.printStackTrace();
}
return false;
}
/**
* 从playload中获取用户的信息
*
* **@param **token jwt
* **@return **用户信息
*/
public String getUserInfo(String token) {
//创建jwt验签对象
JWTVerifier jwtVerifier = JWT._require_(Algorithm._HMAC256_(jwtSecretKey)).build();
try {
//验签
DecodedJWT decodedJWT = jwtVerifier.verify(token);
//获取payload中userInfo的值,并返回
return decodedJWT.getClaim("userInfo").asString();
} catch (JWTVerificationException e) {
e.printStackTrace();
}
return null;
}
/**
* 获取用户权限
*
* **@param **token
* **@return
***/
public List<String> getUserAuth(String token) {
//创建jwt验签对象
JWTVerifier jwtVerifier = JWT._require_(Algorithm._HMAC256_(jwtSecretKey)).build();
try {
//验签
DecodedJWT decodedJWT = jwtVerifier.verify(token);
//获取payload中的自定义数据authList(权限列表),并返回
return decodedJWT.getClaim("authList").asList(String.class);
} catch (JWTVerificationException e) {
e.printStackTrace();
}
return null;
}
}
添加响应类
com.powernode.vo包中
package com.powernode.vo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class HttpResult implements Serializable {
private Integer code; //响应码
private String msg; //响应消息
private Object data; //响应对象
}
修改SecurityUser类
加入一个获取SysUser的方法,后面会用到这个方法
public class MySecurityUser implements UserDetails {
private final SysUser sysUser;
//用于存储权限的list
private List<SimpleGrantedAuthority> authorityList;
public MySecurityUser(SysUser sysUser){
this.sysUser=sysUser;
}
public SysUser getSysUser() {
return sysUser;
}
/**
* 返回用户所拥有的权限
* @return
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorityList;
}
public void setAuthorityList(List<SimpleGrantedAuthority> authorityList) {
this.authorityList = authorityList;
}
@Override
public String getPassword() {
String myPassword=sysUser.getPassword();
sysUser.setPassword(null); //擦除我们的密码,防止传到前端
return myPassword;
}
@Override
public String getUsername() {
return this.sysUser.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return sysUser.getAccountNoExpired().equals(1);
}
@Override
public boolean isAccountNonLocked() {
return sysUser.getAccountNoLocked().equals(1);
}
@Override
public boolean isCredentialsNonExpired() {
return sysUser.getCredentialsNoExpired().equals(1);
}
@Override
public boolean isEnabled() {
return sysUser.getEnabled().equals(1);
}
}
新建认证成功处理器
认证成功处理器,当用户登录成功后,会执行此处理器将获取的登录用户信息放入生成的jwt中返回给前端,此后每次请求就都带着token
/**
* 认证成功处理器,当用户登录成功后,会执行此处理器将获取的登录用户信息放入生成的jwt中返回给前端,此后每次请求就都带着token
*/
@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
//使用此工具类进行序列化
@Resource
private ObjectMapper objectMapper;
@Resource
private JwtUtils jwtUtils;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=utf-8");
//从认证对象中获取认证用户信息
SecurityUser securityUser = (SecurityUser) authentication.getPrincipal();
String userInfo=objectMapper.writeValueAsString(securityUser.getSysUser());
List<SimpleGrantedAuthority> authorities = (List<SimpleGrantedAuthority>) securityUser.getAuthorities();
//_这可以改成stream流
List<String> authList=new ArrayList<>();
for (SimpleGrantedAuthority authority : authorities) {
authList.add(authority.getAuthority());
}//使用stream流1,使用lambda表达式
List<String> test = authorities.stream().map(
a -> {
return a.getAuthority();
}
).collect(Collectors._toList_());
System._out_.println("test = " + test);
//使用stream流2
List<String> test111 = authorities.stream().map(SimpleGrantedAuthority::getAuthority).collect(Collectors._toList_());
System._out_.println("test111 = " + test111);
// 创建jwt
String token = jwtUtils.createToken(userInfo,authList);
//返回给前端token这里用统一格式
HttpResult httpResult = HttpResult._builder_().code(200).msg("OK").data(token).build();
PrintWriter writer = response.getWriter();
将生成好的jwt包装到httpResult这个响应类中转为json格式发送到前端
writer.write(objectMapper.writeValueAsString(httpResult));
writer.flush();
}
}
新建jwt过滤器,用于检查token等
用户登录成功经过认证成功处理器处理后产生一个jwt发送到前端后,前端每次请求后端都需要经过验证来确认是否是本人
com.powernode.filter包中新建类
/**
* 定义一次性请求过滤器
*/
@Component
@Slf4j
public class JwtCheckFilter extends OncePerRequestFilter {
@Resource
private ObjectMapper objectMapper;
@Resource
private JwtUtils jwtUtils;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//获取请求头中的Authorization
String authorization = request.getHeader("Authorization");
//如果Authorization为空,那么不允许用户访问,直接返回
if (!StringUtils._hasText_(authorization)) {
printFront(response, "没有登录!");
return;
}
//Authorization 去掉头部的Bearer 信息,获取token值
String jwtToken = authorization.replace("Bearer ", "");
//验证签名(简称验签)
boolean verifyTokenResult = jwtUtils.verifyToken(jwtToken);
//验签不成功
if (!verifyTokenResult) {
printFront(response, "jwtToken 已过期");
return;
}
//从payload中获取userInfo
String userInfo = jwtUtils.getUserInfo(jwtToken);
//从payload中获取授权列表
List<String> userAuth = jwtUtils.getUserAuth(jwtToken);
//创建登录用户
SysUser sysUser = objectMapper.readValue(userInfo, SysUser.class);
SecurityUser securityUser = new SecurityUser(sysUser);
//设置权限
List<SimpleGrantedAuthority> authList = userAuth.stream().map(SimpleGrantedAuthority::new).collect(Collectors._toList_());
securityUser.setAuthorityList(authList);
//创建用户名密码token
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToke = new UsernamePasswordAuthenticationToken(securityUser, null, authList);
//通过安全上下文设置认证信息
SecurityContextHolder._getContext_().setAuthentication(usernamePasswordAuthenticationToke);
//继续访问相应的url等
filterChain.doFilter(request, response);
}
private void printFront(HttpServletResponse response, String message) throws IOException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
HttpResult httpResult =HttpResult.builder().code(-1).msg(message).build();
writer.print(objectMapper.writeValueAsString(httpResult));
writer.flush();
} @Override protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { //如果是登陆请求,直接放行,不走过滤器 return request.getRequestURI().equals("/login"); }
}
修改 web安全配置类WebSecurityConfig
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Resource
private JwtCheckFilter jwtCheckFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterBefore(jwtCheckFilter, UsernamePasswordAuthenticationFilter.class);
http.formLogin().successHandler(myAuthenticationSuccessHandler).permitAll();
http.authorizeRequests()
.mvcMatchers("/student/**").hasAnyAuthority("student:query","student:update")
.anyRequest().authenticated(); //任何请求均需要认证(登录成功)才能访问//禁用跨域请求保护
http.csrf().disable();
//禁用session
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy._STATELESS_);
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
启动测试
先登录系统,获取页面上返回的token,然后使用postman 在请求头中携带token发送请求即可。
启动redis并使用客户端工具连接到redis
测试后的问题:用户退出的问题
问题:因为JWT无状态,导致退出功能无法实现:用户退出后,因为JWT的过期时间不能被外部干扰,导致jwt仍然可用,用户退出后,客户端发送请求仍然可以访问服务端。
解决办法:
使用redis
步骤:
① 登陆成功之后把生成JWT存到redis中
key | value |
---|---|
logintoken:jwt | 认证信息authentication |
② 用户退出时,从redis中删除该token
③ 用户每次访问时,先校验jwt是否合法,如果合法再从redis里面取出logintoken:jwt判断这个jwt还存不存在,如果不存在就说明用户已经退出来,就返回未登陆。
复制工程
复制springsecurity-16-jwt-authentication 成springsecurity-16-jwt-authentication-redis工程
加入redis依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置redis信息
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/security_study?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: 123
redis:
host: 192.168.126.130//自己配置
port: 6379
password: 123456
database: 0
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.powernode.entity
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
my:
secretKey: thomas12345678
修改认证成功处理器
在创建jwt时存入redis中,
@Component
@Slf4j
public class MyAutheticationSuccessHandler implements AuthenticationSuccessHandler {
@Resource
private JwtUtils jwtUtils;
@Resource
private ObjectMapper objectMapper;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//从认证信息中获取登录用户信息
MySecurityUser securityUser= (MySecurityUser) authentication.getPrincipal();
SysUser sysUser = securityUser.getSysUser();
String strUserInfo = objectMapper.writeValueAsString(sysUser);
//获取用户的权限信息
List<SimpleGrantedAuthority> authorities = (List<SimpleGrantedAuthority>) securityUser.getAuthorities();
//SimpleGrantedAuthority::getAuthority 调用SimpleGrantedAuthority 类的getgetAuthority方法
// collect 收集
List<String> authList=authorities.stream().map(SimpleGrantedAuthority::getAuthority).collect(Collectors.toList());
//生成jwt
String jwtToken = jwtUtils.createJwt(strUserInfo, authList);
HttpResult httpResult=HttpResult.builder()
.code(1)
.msg("jwt生成成功")
.data(jwtToken)
.build();
//将jwt放到redis,设置过期时间和jwt的过期时间一样
stringRedisTemplate.opsForValue().set("logintoken:"+jwtToken,objectMapper.writeValueAsString(authentication),2, TimeUnit.HOURS);
printToken(request,response,httpResult);
}
private void printToken(HttpServletRequest request, HttpServletResponse response,HttpResult httpResult) throws IOException {
String strResponse = objectMapper.writeValueAsString(httpResult);
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.println(strResponse);
writer.flush();
}
}
新建用户退出成功处理器
用户退出时,会将redis中的jwt删除,这样前端再次请求后,先会判断jwt是否合法,合法后会去判断redis中的jwt是否存在,不存在说明用户已经退出了,就返回前端信息表示账户未登录
逻辑是:先判断jwt是否存在和jwt是否合法,如果不存在或者不合法就不用执行redis中的删除操作
@Component
@Slf4j
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private JwtUtils jwtUtils;
@Resource
private ObjectMapper objectMapper;
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
String auth = request.getHeader("Authorization");
if (StringUtils.isEmpty(auth)) {
HttpResult httpResult = HttpResult.builder()
.code(0)
.msg("jwt 不存在")
.build();
printFront(request, response, httpResult);
return;
}
String jwtToken = auth.replace("bearer ", "");
boolean result = jwtUtils.verifyToken(jwtToken);
if (!result) {
HttpResult httpResult = HttpResult.builder()
.code(0)
.msg("jwt 非法")
.build();
printFront(request, response, httpResult);
return;
}
//从redis中删除登录成功后放入的jwttoken
stringRedisTemplate.delete("logintoken:" + jwtToken);
HttpResult okResult = HttpResult.builder()
.code(1)
.msg("退出成功")
.build();
printFront(request,response,okResult);
}
/**
* 响应前端
*
* @param request
* @param response
* @param httpResult
* @throws IOException
*/
private void printFront(HttpServletRequest request, HttpServletResponse response, HttpResult httpResult) throws IOException {
String strResponse = objectMapper.writeValueAsString(httpResult);
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.println(strResponse);
writer.flush();
}
}
配置用户成功退出处理器修改WebSecurityConfig
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Slf4j
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private MyAutheticationSuccessHandler myAutheticationSuccessHandler;
@Resource
private JwtCheckFilter jwtCheckFilter;
@Resource
private MyLogoutSuccessHandler myLogoutSuccessHandler;
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterBefore(jwtCheckFilter, UsernamePasswordAuthenticationFilter.class);
http.authorizeRequests().anyRequest().authenticated();
http.formLogin().successHandler(myAutheticationSuccessHandler).permitAll();
http.logout().logoutSuccessHandler(myLogoutSuccessHandler);
http.csrf().disable(); //禁用跨域请求保护
//不创建session
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
修改jwtcheckfilter
@Component
@Slf4j
public class JwtCheckFilter extends OncePerRequestFilter {
@Resource
private JwtUtils jwtUtils;
@Resource
private ObjectMapper objectMapper;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String requestURI = request.getRequestURI();
//如果是登录请求urI,直接放行
if(requestURI.equals("/login")){
doFilter(request,response,filterChain);
return;
}
String strAuth = request.getHeader("Authorization");
if(StringUtils.isEmpty(strAuth)){
HttpResult httpResult = HttpResult.builder()
.code(0)
.msg("Authorization 为空")
.build();
printToken(request,response, httpResult);
return;
}
String jwtToken = strAuth.replace("bearer ", "");
if(StringUtils.containsWhitespace(jwtToken)){
HttpResult httpResult = HttpResult.builder()
.code(0)
.msg("jwt 为空")
.build();
printToken(request,response, httpResult);
return;
}
//校验jwt
boolean verifyResult = jwtUtils.verifyToken(jwtToken);
if(!verifyResult){
HttpResult httpResult = HttpResult.builder()
.code(0)
.msg("jwt非法!!!!")
.build();
printToken(request,response, httpResult);
return;
}
//判断redis是否存在jwt
String redisToken = stringRedisTemplate.opsForValue().get("logintoken:" + jwtToken);
if(StringUtils.isEmpty(redisToken)){
HttpResult httpResult = HttpResult.builder()
.code(0)
.msg("您已经退出,请重新登录!!!!")
.build();
printToken(request,response, httpResult);
return;
}
//从jwt里获取用户信息和权限信息
String userInfo = jwtUtils.getUserInfoFromToken(jwtToken);
List<String> userAuthList = jwtUtils.getUserAuthFromToken(jwtToken);
//反序列化成SysUser对象
SysUser sysUser = objectMapper.readValue(userInfo, SysUser.class);
List<SimpleGrantedAuthority> authorityList=userAuthList.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
//用户名密码认证token
UsernamePasswordAuthenticationToken token=new UsernamePasswordAuthenticationToken(sysUser,null,authorityList);
//把token放到安全上下文:securityContext
SecurityContextHolder.getContext().setAuthentication(token);
doFilter(request,response,filterChain); //放行
}
private void printToken(HttpServletRequest request, HttpServletResponse response, HttpResult httpResult) throws IOException {
String strResponse = objectMapper.writeValueAsString(httpResult);
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.println(strResponse);
writer.flush();
}
}
启动程序并登录测试
登录后查看redis中是否存储了token
使用jwt token访问/student/query
使用postman测试,发现可以正常访问
使用postman 退出系统
注意携带token,才能退出啊
注意:要禁用跨域请求保护,要不然使用postman无法访问logout端点
http.csrf().disable(); //禁用跨域请求保护
再次使用token访问/student/query
现象:发现已经不能正常访问了
原因:虽然token本身并没有过期,但是redis中已经删除了该token,所以不能正常访问了
使用curl 访问
使用debug模式,理解整个流程