Spring Security 核心概念与配置
Spring Security
官网地址:https://spring.io/projects/spring-security
Spring Security是一个能够为基于Spring的企业应用系统提供声明式(注解)的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
以上解释来源于百度百科。可以一句话来概括:
SpringSecurity 是一个安全框架。
安全框架主要有三种方式实现
Ø Shiro:轻量级的安全框架,提供认证、授权、会话管理、密码管理、缓存管理等功能
Ø Spring Security:功能比Shiro强大,更复杂,权限控制细粒度更高,对OAuth2 支持更好,与Spring 框架无缝集合,使Spring Boot 集成很快捷。
自己写:基于过滤器(filter)和AOP来实现,难度大,没必要。
安全入门项目
直接添加依赖即可使用,访问资源时会跳到login页面,默认用户名为user,密码在控制台输出。地址栏输入logout退出。
引入spring-boot-starter-security依赖后,项目中除登录退出外所有资源都会被保护起来
认证(登录)用户可以访问所有资源,不经过认证用户任何资源也访问不了。
所有资源均已保护,但是用户只用一个,密码是随机的,只能在开发环境使用。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.ldustu</groupId>
<artifactId>demo01</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.9</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
</project>
配置文件配置用户名密码
配置application.yml即可,Spring Security配置文件中默认配置用户是单一的用户,大部分系统都有多个用户,多个用户如何配置?
spring:
security:
user:
name: admin
password: 123
新建配置类配置用户名密码
新建配置类。**注意:**此时配置文件中配置的用户名和密码不会起作用。
package com.ldustu.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
@Configuration
@Slf4j
public class MySecurityUserConfig {
@Bean
public UserDetailsService userDetailsService(){
//该接口实现了UserDetailManager
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
//定义三个用户对象
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("obama")
.password(passwordEncoder().encode("123456"))
.roles("teacher")
.build();
//创建三个用户
manager.createUser(user1);
manager.createUser(user2);
manager.createUser(user3);
return manager;
}
/**
* passwordEncoder security框架使用它,判断用户输入的密码和系统定义的密码是否一致
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
//不编码的编码器
//return NoOpPasswordEncoder.getInstance();
return new BCryptPasswordEncoder();
}
}
源码
UserDetail
用户详情接口
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package org.springframework.security.core.userdetails;
import java.io.Serializable;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
public interface UserDetails extends Serializable {
//权限集合
Collection<? extends GrantedAuthority> getAuthorities();
//获取该用户的密码
String getPassword();
//用户名
String getUsername();
//用户是否未过期
boolean isAccountNonExpired();
//用户是否未锁定
boolean isAccountNonLocked();
//用户是否凭据未过期
boolean isCredentialsNonExpired();
//用户是否可以使用
boolean isEnabled();
}
UserDetailsService
下面有一个接口UserDetailManager,以及三个实现类JdbcDaoImpl、UserDetailsServiceDelegator、CachingUserDetailsService实现了该接口。
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package org.springframework.security.core.userdetails;
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
PasswordEncoder
密码加密接口
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package org.springframework.security.crypto.password;
public interface PasswordEncoder {
String encode(CharSequence rawPassword);
boolean matches(CharSequence rawPassword, String encodedPassword);
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
用户权限接口
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package org.springframework.security.core;
import java.io.Serializable;
public interface GrantedAuthority extends Serializable {
String getAuthority();
}
密码处理
为什么要加密?
csdn 密码泄露事件
泄露事件经过:https://www.williamlong.info/archives/2933.html
泄露数据分析:https://blog.csdn.net/crazyhacking/article/details/10443849
密码加密一般使用散列函数,又称散列算法,哈希函数,这些函数都是单向函数(从明文到密文,反之不行)
常用的散列算法有MD5和SHA
Spring Security提供多种密码加密方案,基本上都实现了PasswordEncoder接口,官方推荐使用BCryptPasswordEncoder
断言
开发代码时不允许使用main方法测试,而是使用单元测试来测试
代码中一般不允许使用System.out.println 直接输出,而是使用日志输出
单元测试尽量使用断言,而不是使用System.out.println输出
package com.ldustu;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@Slf4j
public class PasswordEncoderTest {
@Test
void testLearn(){
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
String cipherText1 = encoder.encode("123456");
String cipherText2 = encoder.encode("123456");
String cipherText3 = encoder.encode("123456");
log.info("c1: {}",cipherText1);
//$2a$10$FJzXcpXkb9pFnaBMicXe4uzZHZ1r03Y.ElVoGKfAAGBo1pNk3Ttum
log.info("c2: {}",cipherText2);
//$2a$10$W61SbDp9ukUxafisMR/pqOrpb2YPNtBkii.fzjaWau1aYBDa2/rTC
log.info("c3: {}",cipherText3);
//$2a$10$z6GJYYSFpvYe1IBm12VvOeHUeg/UvtJCF9Zk7pPxtFcZGUEGZm/Om
boolean matches1 = encoder.matches("123456", cipherText1);
boolean matches2 = encoder.matches("123456", cipherText2);
boolean matches3 = encoder.matches("123456", cipherText3);
// log.info(matches1+"");
// log.info(matches2+"");
// log.info(matches3+"");
Assertions.assertTrue(matches1);
Assertions.assertTrue(matches2);
Assertions.assertTrue(matches3);
}
}
加盐
相同的字符串加密之后的结果都不一样,但是比较的时候是一样的,因为加了盐(salt)了。
获取用户信息
package com.ldustu.controller;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.security.Principal;
@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;
}
}
登陆成功后,密码会被擦除。
{"authorities":[{"authority":"ROLE_student"}],"details":{"remoteAddress":"127.0.0.1","sessionId":"D3DCF45FC31D935D9E15B964876F6259"},"authenticated":true,"principal":{"password":null,"username":"eric","authorities":[{"authority":"ROLE_student"}],"accountNonExpired":true,"accountNonLocked":true,"credentialsNonExpired":true,"enabled":true},"credentials":null,"name":"eric"}
Ø Principal 定义认证的而用户,如果用户使用用户名和密码方式登录,principal通常就是一个UserDetails(后面再说)
Ø Credentials:登录凭证,一般就是指密码。当用户登录成功之后,登录凭证会被自动擦除,以防泄露。
Ø authorities:用户被授予的权限信息。
认证授权
认证 :用户认证就是判断一个用户的身份是否合法的过程。
系统为什么要认证?
认证是为了保护系统的隐私数据与资源,用户的身份合法方可访问该系统的资源。
常见的用户身份认证方式
Ø 用户名密码登录
Ø 二维码登录
Ø 手机短信登录
Ø 指纹认证
Ø 人脸识别
Ø 等等…
会话(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) 基于角色的访问控制
用户,角色,权限 本质:就是把权限打包给角色(角色拥有一组权限),分配给用户(用户拥有多个角色)。
最少包括五张表 (用户表、角色表、用户角色表,权限表,用户权限表)
权限
配置权限
配置用户权限有两种方式:
Ø 配置roles
Ø 配置authorities
注意事项:
Ø 如果给一个用户同时配置roles和authorities,哪个写在后面哪个起作用
Ø 配置roles时,权限名会加上ROLE_。
从设计层面讲,角色和权限是两个完全不同的东西
从代码层面来讲,角色和权限并没有太大区别,特别是在Spring Security中
// 注意 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"))
.roles("teacher")
.authorities("teacher:add","teacher:update")
.build();
授权
对URL进行授权
上面讲的实现了认证功能,但是受保护的资源是默认的,默认所有认证(登录)用户均可以访问所有资源,不能根据实际情况进行角色管理,要实现授权功能,需重写WebSecurityConfigureAdapter 中的一个configure方法
新建WebSecurityConfig类,重写configure(HttpSecurity http)方法
package com.ldustu.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
@Slf4j
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() //授权请求
//匹配器
// .regexMatchers() 正则匹配器
// .antMatchers() ant匹配器
.mvcMatchers("/student/**") //mvc匹配器
//设置对应的角色或权限
// .access("hasAnyRole('teacher') or hasAnyAuthority('ROLE_amdin')") //最强大,不建议使用
// .hasAnyRole() 具有任意角色即可访问
// .hasAnyAuthority() 具有任意权限即可访问
// .hasAuthority() 具有该权限才可访问
.hasRole("student")
.mvcMatchers("/teacher/**")
.hasAuthority("ROLE_teacher") //设置该权限可以访问
.anyRequest() //任何请求
.authenticated(); //均需要认证
http.formLogin(); //使用表单方式登陆
// .denyAll() 拒绝所有
// .permitAll() 允许所有
}
}
方法级别的权限控制
上面学习的认证与授权都是基于URL的,我们也可以通过注解灵活的配置方法安全,我们先通过@EnableGlobalMethodSecurity开启基于注解的安全配置。
Ø EnableGlobalMethodSecurity注解的属性prePostEnabled = true 解锁@PreAuthorize 和@PostAuthorize注解,@PreAuthorize 在方法执行前进行验证,@PostAuthorize 在方法执行后进行验证
Ø EnableGlobalMethodSecurity的securedEnabled = true 解锁@Secured注解,@Secured和@PreAuthorize用法基本一样 @Secured对应的角色必须要有ROLE_前缀
1、开启注解的安全配置
package com.ldustu.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
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;
@Configuration
@Slf4j
@EnableGlobalMethodSecurity(prePostEnabled = true) //加上启用全局方法安全注解
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() //授权请求
.anyRequest() //任何请求
.authenticated(); //均需要认证
http.formLogin(); //使用表单方式登陆
}
}
2、在方法上添加角色或权限
package com.ldustu.controller;
import com.ldustu.service.TeacherService;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@RequestMapping("/teacher")
public class TeacherController {
@Resource
private TeacherService teacherService;
@GetMapping("/query")
@PreAuthorize("hasRole('student')") //其中方法与url授权一样
public String queryInfo() {
return teacherService.query();
}
@GetMapping("/add")
@PreAuthorize("hasRole('teacher')")
public String addInfo() {
return teacherService.add();
}
@GetMapping("/update")
@PreAuthorize("hasRole('teacher')")
public String updateInfo() {
return teacherService.update();
}
@GetMapping("/delete")
@PreAuthorize("hasRole('student')")
public String deleteInfo() {
return teacherService.delete();
}
}
返回json
根据不同场景返回json串
登陆成功
package com.ldustu.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ldustu.vo.HttpResult;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class MyAuthenticationSuccessHandle implements AuthenticationSuccessHandler {
@Resource
private ObjectMapper objectMapper;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=utf-8");
HttpResult httpResult = new HttpResult(200, "登录成功", authentication);
String str = objectMapper.writeValueAsString(httpResult);
response.getWriter().write(str);
response.getWriter().flush();
}
}
登陆失败
package com.ldustu.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ldustu.vo.HttpResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.*;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 登陆失败的处理器
*/
@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 {
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();
response.getWriter().write(json);
response.getWriter().flush();
}
}
退出成功
package com.ldustu.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ldustu.vo.HttpResult;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 退出成功的处理器
*/
@Component
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
//声明一个把对象转成JSON的对象
@Resource
private ObjectMapper objectMapper;
/**
*
* @param request
* @param response
* @param authentication 当前退出的用户对象
* @throws IOException
*/
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
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();
response.getWriter().write(json);
response.getWriter().flush();
}
}
拒绝访问
package com.ldustu.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ldustu.vo.HttpResult;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 无权限的处理器
*/
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
//声明一个把对象转成JSON的对象
@Resource
private ObjectMapper objectMapper;
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
//设置响应编码
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();
response.getWriter().write(json);
response.getWriter().flush();
}
}
绑定处理器
package com.ldustu.config;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
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;
@Configuration
@Slf4j
@RequiredArgsConstructor
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private final MyLogoutSuccessHandler myLogoutSuccessHandler;
private final MyAuthenticationSuccessHandle myAuthenticationSuccessHandle;
private final MyAuthenticationFailureHandler myAuthenticationFailureHandler;
private final MyAccessDeniedHandler myAccessDeniedHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() //授权请求
.anyRequest() //任何请求
.authenticated(); //均需要认证
http.formLogin() //使用表单方式登陆
.successHandler(myAuthenticationSuccessHandle) //登录成功处理器
.failureHandler(myAuthenticationFailureHandler); //登陆失败处理器
http.logout().logoutSuccessHandler(myLogoutSuccessHandler); //退出成功处理器
http.exceptionHandling().accessDeniedHandler(myAccessDeniedHandler); //拒绝访问处理器(无权限)
}
}
自定义UserDetail
1、实现UserDetail并添加权限
package com.ldustu.vo;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public class SecurityUser implements UserDetails {
//添加权限
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
GrantedAuthority g1=()->"student"; //使用lambda表达式创建接口实现类,而不是使用匿名内部类来实现接口
// GrantedAuthority g1=new SimpleGrantedAuthority("student:query"); // 使用子类创建对象
List<GrantedAuthority> grantedAuthorityList=new ArrayList<>();
grantedAuthorityList.add(g1);
return grantedAuthorityList;
}
@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;
}
}
2、实现UserDetailService
package com.ldustu.service.impl;
import com.ldustu.vo.SecurityUser;
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;
@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;
}
}
3、编码器注册到spring容器中
package com.ldustu;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class,args);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
基于数据库
认证
1、新建数据库security_study,建表添加数据
/*
Navicat Premium Data Transfer
Source Server : myhost
Source Server Type : MySQL
Source Server Version : 80031
Source Host : localhost:3306
Source Schema : security_study
Target Server Type : MySQL
Target Server Version : 80031
File Encoding : 65001
Date: 22/11/2022 09:11:07
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for sys_menu
-- ----------------------------
DROP TABLE IF EXISTS `sys_menu`;
CREATE TABLE `sys_menu` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '编号',
`pid` int NULL DEFAULT NULL COMMENT '父级编号',
`name` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '名称',
`code` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '权限编码',
`type` int NULL DEFAULT NULL COMMENT '0代表菜单1权限2 url',
`delete_flag` tinyint NULL DEFAULT 0 COMMENT '0代表未删除,1 代表已删除',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 10 CHARACTER SET = utf8mb3 COLLATE = utf8mb3_general_ci ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Records of sys_menu
-- ----------------------------
INSERT INTO `sys_menu` VALUES (1, 0, '学生管理', '/student/**', 0, 0);
INSERT INTO `sys_menu` VALUES (2, 1, '学生查询', 'student:query', 1, 0);
INSERT INTO `sys_menu` VALUES (3, 1, '学生添加', 'student:add', 1, 0);
INSERT INTO `sys_menu` VALUES (4, 1, '学生修改', 'student:update', 1, 0);
INSERT INTO `sys_menu` VALUES (5, 1, '学生删除', 'student:delete', 1, 0);
INSERT INTO `sys_menu` VALUES (6, 1, '导出学生信息', 'student:export', 1, 0);
INSERT INTO `sys_menu` VALUES (7, 0, '教师管理', '/teacher/**', 0, 0);
INSERT INTO `sys_menu` VALUES (9, 7, '教师查询', 'teacher:query', 1, 0);
-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '角色ID',
`rolename` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '角色名称,英文名称',
`remark` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb3 COLLATE = utf8mb3_general_ci ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Records of sys_role
-- ----------------------------
INSERT INTO `sys_role` VALUES (1, 'ROLE_ADMIN', '管理员');
INSERT INTO `sys_role` VALUES (2, 'ROLE_TEACHER', '老师');
INSERT INTO `sys_role` VALUES (3, 'ROLE_STUDENT', '学生');
-- ----------------------------
-- Table structure for sys_role_menu
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_menu`;
CREATE TABLE `sys_role_menu` (
`rid` int NOT NULL COMMENT '角色表的编号',
`mid` int NOT NULL COMMENT '菜单表的编号',
PRIMARY KEY (`mid`, `rid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb3 COLLATE = utf8mb3_general_ci ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Records of sys_role_menu
-- ----------------------------
INSERT INTO `sys_role_menu` VALUES (1, 1);
INSERT INTO `sys_role_menu` VALUES (3, 1);
INSERT INTO `sys_role_menu` VALUES (2, 2);
INSERT INTO `sys_role_menu` VALUES (3, 2);
INSERT INTO `sys_role_menu` VALUES (1, 3);
INSERT INTO `sys_role_menu` VALUES (2, 3);
INSERT INTO `sys_role_menu` VALUES (1, 4);
INSERT INTO `sys_role_menu` VALUES (2, 4);
INSERT INTO `sys_role_menu` VALUES (1, 5);
INSERT INTO `sys_role_menu` VALUES (2, 5);
INSERT INTO `sys_role_menu` VALUES (3, 6);
INSERT INTO `sys_role_menu` VALUES (1, 9);
INSERT INTO `sys_role_menu` VALUES (2, 9);
INSERT INTO `sys_role_menu` VALUES (3, 9);
INSERT INTO `sys_role_menu` VALUES (1, 10);
INSERT INTO `sys_role_menu` VALUES (1, 17);
-- ----------------------------
-- Table structure for sys_role_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_user`;
CREATE TABLE `sys_role_user` (
`uid` int NOT NULL COMMENT '用户编号',
`rid` int NOT NULL COMMENT '角色编号',
PRIMARY KEY (`uid`, `rid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb3 COLLATE = utf8mb3_general_ci ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Records of sys_role_user
-- ----------------------------
INSERT INTO `sys_role_user` VALUES (1, 1);
INSERT INTO `sys_role_user` VALUES (2, 2);
INSERT INTO `sys_role_user` VALUES (3, 3);
-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
`user_id` int NOT NULL AUTO_INCREMENT COMMENT '编号',
`username` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '登陆名',
`password` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '密码',
`sex` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '性别',
`address` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '地址',
`enabled` int NULL DEFAULT 1 COMMENT '是否启动账户0禁用 1启用',
`account_no_expired` int NULL DEFAULT 1 COMMENT '账户是否没有过期0已过期 1 正常',
`credentials_no_expired` int NULL DEFAULT 1 COMMENT '密码是否没有过期0已过期 1 正常',
`account_no_locked` int NULL DEFAULT 1 COMMENT '账户是否没有锁定0已锁定 1 正常',
PRIMARY KEY (`user_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb3 COLLATE = utf8mb3_general_ci ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Records of sys_user
-- ----------------------------
INSERT INTO `sys_user` VALUES (1, 'obama', '$2a$10$KyXAnVcsrLaHMWpd3e2xhe6JmzBi.3AgMhteFq8t8kjxmwL8olEDq', '男', '武汉', 1, 1, 1, 1);
INSERT INTO `sys_user` VALUES (2, 'thomas', '$2a$10$KyXAnVcsrLaHMWpd3e2xhe6JmzBi.3AgMhteFq8t8kjxmwL8olEDq', '男', '北京', 1, 1, 1, 1);
INSERT INTO `sys_user` VALUES (3, 'eric', '$2a$10$KyXAnVcsrLaHMWpd3e2xhe6JmzBi.3AgMhteFq8t8kjxmwL8olEDq', '男', '成都', 1, 1, 1, 1);
SET FOREIGN_KEY_CHECKS = 1;
2、新建maven工程,添加依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.ldustu</groupId>
<artifactId>mysql_security</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.9</version>
</parent>
<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>
<version>5.1.47</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>2.3.3</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>
</project>
3、配置application.yml
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/security_study?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: ldustu2022
mybatis-plus:
type-aliases-package: com.ldustu.entity
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mapper-locations: classpath:mapper/*.xml
4、使用mybatis-plus插件生成sys_user表的映射实体类与mapper
5、创建测试类测试查询sys_user表中的数据
package com.ldustu.dao;
import com.ldustu.entity.SysUser;
import lombok.RequiredArgsConstructor;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class SysUserMapperTest {
@Resource
private SysUserMapper sysUserMapper;
@Test
void getByUserName() {
SysUser sysUser = sysUserMapper.getByUserName("thomas");
assertNotNull(sysUser);
}
}
6、在vo包下创建securityUser实现UserDetails
package com.ldustu.vo;
import com.ldustu.entity.SysUser;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
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);
}
}
7、在service.impl包下创建UserServiceImpl实现UserDetailService
package com.ldustu.service.impl;
import com.ldustu.dao.SysUserMapper;
import com.ldustu.entity.SysUser;
import com.ldustu.vo.SecurityUser;
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 javax.annotation.Resource;
@Service
public class UserServiceImpl implements UserDetailsService {
@Resource
private SysUserMapper sysUserDao;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser sysUser = sysUserDao.getByUserName(username);
if(null==sysUser){
throw new UsernameNotFoundException("账号不存在");
}
return new SecurityUser(sysUser);
}
}
8、在config包下创建WebSecurityConfig 继承WebSecurityConfigurerAdapter
package com.ldustu.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@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();
}
}
9、在controller包下创建teacher接口进行测试
package com.ldustu.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Slf4j
@RequestMapping("/teacher")
public class TeacherController {
@GetMapping("/query")
@PreAuthorize("hasAuthority('teacher:query')")
public String queryInfo(){
return "I am a teacher!";
}
@GetMapping("test")
public String test() {
return "test";
}
}
10、暂时还没有在securityUser中给加权限,因此只能访问test
授权
基于认证代码进行修改
1、使用插件生成sys_menu表的映射实体类与mapper文件并添加根据用户id查权限集合的方法。
package com.ldustu.dao;
import com.baomidou.mybatisplus.mapper.BaseMapper;
import com.ldustu.entity.SysMenu;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* @author 86188
* @description 针对表【sys_menu】的数据库操作Mapper
* @createDate 2023-06-14 18:44:23
* @Entity com.ldustu.entity.SysMenu
*/
public interface SysMenuMapper extends BaseMapper<SysMenu> {
List<String> queryPermissionByUserId(@Param("userId") Integer userId);
}
<?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.ldustu.dao.SysMenuMapper">
<resultMap id="BaseResultMap" type="com.ldustu.entity.SysMenu">
<id property="id" column="id" jdbcType="INTEGER"/>
<result property="pid" column="pid" jdbcType="INTEGER"/>
<result property="name" column="name" jdbcType="VARCHAR"/>
<result property="code" column="code" jdbcType="VARCHAR"/>
<result property="type" column="type" jdbcType="INTEGER"/>
<result property="deleteFlag" column="delete_flag" jdbcType="TINYINT"/>
</resultMap>
<sql id="Base_Column_List">
id,pid,name,
code,type,delete_flag
</sql>
<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>
2、新建测试类,测试方法
package com.ldustu.dao;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class SysMenuMapperTest {
@Resource
private SysMenuMapper sysMenuDao;
@Test
void queryPermissionByUserId() {
List<String> menuList = sysMenuDao.queryPermissionByUserId(1);
assertTrue(!menuList.isEmpty());
}
}
3、在SecurityUser中添加权限集合变量以及修改权限的方法
package com.ldustu.vo;
import com.ldustu.entity.SysUser;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
public class SecurityUser implements UserDetails {
private final SysUser sysUser;
//添加权限集合变量
private List<SimpleGrantedAuthority> simpleGrantedAuthorities;
public SecurityUser(SysUser sysUser) {
this.sysUser = sysUser;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return simpleGrantedAuthorities;
}
//新添加set方法用于赋权
public void setSimpleGrantedAuthorities(List<SimpleGrantedAuthority> simpleGrantedAuthorities) {
this.simpleGrantedAuthorities = simpleGrantedAuthorities;
}
@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);
}
}
4、在UserServiceImpl中通过权限dao查询用户所属权限并赋权
package com.ldustu.service.impl;
import com.ldustu.dao.SysMenuMapper;
import com.ldustu.dao.SysUserMapper;
import com.ldustu.entity.SysUser;
import com.ldustu.vo.SecurityUser;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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 javax.annotation.Resource;
import java.util.List;
import static java.util.stream.Collectors.toList;
@Service
public class UserServiceImpl implements UserDetailsService {
@Resource
private SysUserMapper sysUserDao;
@Resource
private SysMenuMapper 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());
//将一个字符串列表转换成一个GrantedAuthority列表
// List<GrantedAuthority> grantedAuthorityList=new ArrayList<>();
// for (String userMenu : userMenuList) {
// SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(userMenu);
// grantedAuthorityList.add(simpleGrantedAuthority);
// }
//使用stream流来转换
// SimpleGrantedAuthority::new 相当于调用构造方法
List<SimpleGrantedAuthority> grantedAuthorities = strList
.stream().map(SimpleGrantedAuthority::new).collect(toList());
SecurityUser securityUser = new SecurityUser(sysUser);
securityUser.setSimpleGrantedAuthorities(grantedAuthorities);
return securityUser;
}
}
5、测试访问teacher/query
角色表、权限表、角色权限表)。
集成thymeleaf
1、在基于数据库的代码基础上添加thymeleaf模版
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
2、修改application.yml
pring:
thymeleaf:
cache: false # 不使用缓存
check-template: true # 检查thymeleaf模板是否存在
3、新建LoginController
@Controller
@RequestMapping("/login")
public class LoginController {
/**
* 跳转到登陆页面
*/
@RequestMapping("/toLogin")
public String toLogin(){
return "login";
}
}
4、在templates下面创建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 colspan="2">
<button type="submit">登录</button>
</td>
</tr>
</table>
</form>
</body>
5、修改安全配置文件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 {
//设置登陆方式
http.formLogin()//使用用户名和密码的登陆方式
.usernameParameter("uname") //页面表单的用户名的name
.passwordParameter("pwd")//页面表单的密码的name
.loginPage("/login/toLogin") //自己定义登陆页面的地址
.loginProcessingUrl("/login/doLogin")//配置登陆的url
.successForwardUrl("/index/toIndex") //登陆成功跳转的页面
.failureForwardUrl("/login/toLogin")//登陆失败跳转的页面
.permitAll(); //放行和登陆有关的url,别忘了写这个
//配置退出方式
http.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/login/toLogin")
.permitAll();/放行和退出有关的url,别忘了写这个
//配置路径拦截 的url的匹配规则
http.authorizeRequests()
//任何路径要求必须认证之后才能访问
.anyRequest().authenticated();
// 禁用csrf跨站请求攻击 后面可以使用postman工具测试,注意要禁用csrf
http.csrf().disable();
}
}
6、创建IndexController
@Controller
@RequestMapping("/index")
public class IndexController {
/**
* 登录成功后进入主页
*/
@RequestMapping("/toIndex")
public String toIndex(){
return "main";
}
}
7、在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>
8、修改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";
}
}
9、在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>
10、创建403页面
在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>
11、当用户没有某权限时,页面不展示该按钮(简单看下即可)
上一讲里面我们创建的项目里面是当用户点击页面上的链接请求到后台之后没有权限会跳转到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>
集成图片验证码
概述
上一讲里面我们集成了thymeleaf实现在页面链接的动态判断是否显示,那么在实际开发中,我们会遇到有验证码的功能,那么如何处理呢?
复制上一个工程进行开发
具体过程
1、添加依赖(用于生成验证码)
<!--引入hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.3.9</version>
</dependency>
2、添加一个获取验证码的接口
@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);
//放到session中
String captchaCode=circleCaptcha.getCode();
log.info("生成的验证码为:{}",captchaCode);
request.getSession().setAttribute("LOGIN_CAPTCHA_CODE",captchaCode);
ImageIO.write(circleCaptcha.getImage(),"JPEG",response.getOutputStream());
}
}
3、创建验证码过滤器
@Component
@Slf4j
public class ValidateCodeFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
validateCode(request, response,filterChain);
}
// 验证码校验
private void validateCode(HttpServletRequest request, HttpServletResponse response,FilterChain filterChain) throws IOException, ServletException {
//获取用户的code
String enterCaptchaCode = request.getParameter("code");
//session里的code
HttpSession session = request.getSession();
String captchaCodeInSession = (String) session.getAttribute("LOGIN_CAPTCHA_CODE");
log.info("用户输入的验证码为:{},session中的验证码为:{}",enterCaptchaCode,captchaCodeInSession);
//移除错误信息
session.removeAttribute("captchaCodeErrorMsg");
if (!StringUtils.hasText(captchaCodeInSession)) {
session.removeAttribute("LOGIN_CAPTCHA_CODE");
}
if (!StringUtils.hasText(enterCaptchaCode) || !StringUtils.hasText(captchaCodeInSession) || !enterCaptchaCode.equalsIgnoreCase(captchaCodeInSession)) {
//说明验证码不正确,返回登陆页面
session.setAttribute("captchaCodeErrorMsg", "验证码不正确");
//重定向
response.sendRedirect("/login/toLogin");
}else{
filterChain.doFilter(request,response);
}
}
@Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
//如果不是登陆请求,直接放行,不走过滤器
return !request.getRequestURI().equals("/login/doLogin");
}
}
4、修改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();
}
}
5、修改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="zhangsan"></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>
6、 测试登录
Base64
所谓Base64,就是选出64个字符:小写字母a-z、大写字母A-Z、数字0-9、符号"+“、”/“(再加上作为垫字的”=",实际上是使用65个字符,作为一个基本字符集。然后,其它所有符号或者文件都可以转换成这个字符集中的字符。
关于这个编码的规则:
①.把3个字节变成4个字节。
②每76个字符加一个换行符。
③.最后的结束符也要处理。
Linux下用base64命令编解码字符串
编码:
echo -n 'Hello World' | base64
SGVsbG8gV29ybGQ=
解码:
echo -n 'SGVsbG8gV29ybGQ=' | base64 -d
Hello World
备注:
Ø echo 命令是带换行符的
Ø echo -n 不换行输出
Ø echo -n ‘{“alg”:“HS256”,“typ”:“JWT”}’ | base64
base64编解码文件
#base64编码
#用法: base64 待编码的文件名 > 编码后的文件名
base64 1.mp3 > mymp3
#base64 解码
#用法:base64 -d 待解码的文件名 >解码后的文件名
base64 -d mymp3>88.mp3
Base64和Base64Url 的区别
Base64Url是一种在Base64的基础上编码形成新的编码方式。
Base64Url 编码的流程:
1、明文使用BASE64进行编码
2、在Base64编码的基础上进行以下的处理:
- 去除尾部的"="
- 把"+“替换成”-"
- 斜线"/“替换成下划线”_"
JWT
跨域认证问题
互联网服务离不开用户认证。一般流程是下面这样。
-
用户向服务器发送用户名和密码。
-
服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等等。
-
服务器向用户返回一个 jsession_id,写入用户的 Cookie。
-
用户随后的每一次请求,都会通过 Cookie,将 session_id 传回服务器。
-
服务器收到 session_id,找到前期保存的数据,由此得知用户的身份。
这种模式的问题在于,扩展性(scaling)不好。单机当然没有问题,如果是服务器集群,或者是跨域的服务导向架构,就要求 session 数据共享,每台服务器都能够读取 session。
举例来说,A 网站和 B 网站是同一家公司的关联服务。现在要求,用户只要在其中一个网站登录,再访问另一个网站就会自动登录,请问怎么实现?
一种解决方案是 session 数据持久化,写入数据库或别的持久层。各种服务收到请求后,都向持久层请求数据。这种方案的优点是架构清晰,缺点是工程量比较大。另外,持久层万一挂了,就会单点失败。
另一种方案是服务器索性不保存 session 数据了,所有数据都保存在客户端,每次请求都发回服务器。JWT 就是这种方案的一个代表。 服务器不存数据,客户端存,服务器解析就行了
简介
JSON Web Token(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑且独立的方式,用于在各方之间作为JSON对象安全地传输信息。 此信息可以通过数字签名进行验证和信任。 JWT可以使用密钥(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对进行签名。
Ø 官方网址:https://jwt.io/
Ø 调试页面:https://jwt.io/
Ø 学习文档:https://jwt.io/introduction/
用途
授权:这是我们使用JWT最广泛的应用场景。一次用户登录,后续请求将会包含JWT,对于那些合法的token,允许用户连接路由,服务和资源。目前JWT广泛应用在SSO(Single Sign On)(单点登录)上。因为他们开销很小并且可以在不同领域轻松使用。
信息交换:JSON Web Token是一种在各方面之间安全信息传输的好的方式 因为JWT可以签名 - 例如,使用公钥/私钥对 - 您可以确定发件人是他们所说的人。 此外,由于使用标头和有效负载计算签名,您还可以验证内容是否未被篡改。
组成部分
一个JWT由三部分组成,各部分以点分隔:
- Header(头部)-----base64Url编码的Json字符串
- Payload(载荷)—base64url编码的Json字符串
- Signature(签名)—使用指定算法,通过Header和Playload加盐计算的字符串
Header
此部分有两部分组成:
Ø 一部分是token的类型,目前只能是JWT
Ø 另一部分是签名算法,比如HMAC 、 SHA256 、 RSA
示例:
{
"alg":"HS256",
"typ":"JWT"
}
base64编码命令:
echo -n '{"alg":"HS256","typ":"JWT"}' | base64
Payload
token的第二部分是payload(有效负载),其中包含claims(声明)。Claims是关于一个实体(通常是用户)和其他数据类型的声明。
claims有三种类型:registered,public,and private claims。
Ø Registered(已注册的声明):这些是一组预定义声明,不是强制性的,但建议使用,以提供一组有用的,可互操作的声明。 其中一些是:iss(发行人),exp(到期时间),sub(主题),aud(观众)and others。(请注意,声明名称只有三个字符,因为JWT意味着紧凑。)
JWT 规定了7个官方字段,供选用。
iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号
除了官方字段,你还可以在这个部分定义私有字段,下面就是一个例子。
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
注意,JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息(密码,手机号等)放在这个部分。
这个 JSON 对象也要使用 Base64URL 算法转成字符串。
Ø Public(公开声明):这些可以由使用JWT的人随意定义。 但为避免冲突,应在IANA JSON Web Token Registry中定义它们,或者将其定义为包含防冲突命名空间的URI。
Ø private (私人声明):这些声明是为了在同意使用它们的各方之间共享信息而创建的,并且既不是注册声明也不是公开声明。
示例:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
Signature
Signature(保证数据安全性的)
Signature 部分是对前两部分的签名,防止数据篡改。
首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。
HMACSHA256(
base64UrlEncode(header) + “.” +
base64UrlEncode(payload),
secret)
算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。
示例:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
使用方式
客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。
此后,客户端每次与服务器通信,都要带上这个 JWT。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求的头信息Authorization字段里面。
Authorization: Bearer jwt
另一种做法是,跨域的时候,JWT 就放在 POST 请求的数据体里面,一般不用
特点
JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。
JWT 不加密的情况下,不能将秘密数据写入 JWT。
JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。
JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑(JWT的登出问题)。就是因为服务端无状态了
正常情况下 修改了密码后就会跳转到登录页面 :修改成功后清空浏览器保存的token了
后端怎么玩? 因为服务端不保留token 我用之前的token 还是可以继续访问的
从有状态(后端也会存一个)的变成无状态的了
我们就要把它从无状态再变成有状态了
JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。
为了减少盗用,JWT 不应该使用 HTTP 80 协议明码传输,要使用 HTTPS 443 协议传输。
我们颁发一个令牌 用户名称 用户的权限信息 这个令牌2个小时有效
Jwt只要能解析 就认为你是可用的, 做不了退出(logout),后端不存储用户信息了 后端无状态了
代码演示
1、添加依赖
<!-- 添加jwt的依赖 -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>
2、编写功能类
package com.powernode.utils;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 用于生成和解析JWT
*/
public class JWTUtils {
/**
* 声明一个秘钥
*/
private static final String SECRET = "coisinimp";
/**
* 生成JWT
*
* @param userId 用户编号
* @param username 用户名
* @param auth 用户权限
*/
public String createToken(Integer userId, String username, List<String> auth) {
//得到当前的系统时间
Date currentDate = new Date();
//根据当前时间计算出过期时间 定死为5分钟
Date expTime = new Date(currentDate.getTime() + (1000 * 60 * 5));
//组装头数据
Map<String, Object> header = new HashMap<>();
header.put("alg", "HS256");
header.put("typ", "JWT");
return JWT.create()
.withHeader(header) //头
.withClaim("userId", userId) //自定义数据
.withClaim("username", username) //自定义数据
.withClaim("auth", auth) //自定义数据
.withIssuedAt(currentDate) //创建时间
.withExpiresAt(expTime)//过期时间
.sign(Algorithm.HMAC256(SECRET));
}
/**
* 验证JWT并解析
*
* @param token 要验证的jwt的字符串
*/
public static Boolean verifyToken(String token) {
try{
// 使用秘钥创建一个解析对象
JWTVerifier jwtVerifier=JWT.require(Algorithm.HMAC256(SECRET)).build();
//验证JWT
DecodedJWT decodedJWT = jwtVerifier.verify(token);
// String header = decodedJWT.getHeader();
// String payload = decodedJWT.getPayload();
// String signature = decodedJWT.getSignature();
// System.out.println("header = " + header);
// System.out.println("payload = " + payload);
// System.out.println("signature = " + signature);
//
// Date expiresAt = decodedJWT.getExpiresAt();
// System.out.println("expiresAt = " + expiresAt);
// Claim userId = decodedJWT.getClaim("userId");
// System.out.println("userId = " + userId.asInt());
// Claim username = decodedJWT.getClaim("username");
// System.out.println("username = " + username.asString());
// Claim auth = decodedJWT.getClaim("auth");
// System.out.println("auth = " + auth.asList(String.class));
return true;
}catch (TokenExpiredException e){
e.printStackTrace();
}
return false;
}
/**
* 获取JWT里面相前的用户编号
*/
public Integer getUserId(String token){
try{
// 使用秘钥创建一个解析对象
JWTVerifier jwtVerifier=JWT.require(Algorithm.HMAC256(SECRET)).build();
//验证JWT
DecodedJWT decodedJWT = jwtVerifier.verify(token);
Claim userId = decodedJWT.getClaim("userId");
return userId.asInt();
}catch (TokenExpiredException e){
e.printStackTrace();
}
return null;
}
/**
* 获取JWT里面相前的用户名
*/
public static String getUsername(String token){
try{
// 使用秘钥创建一个解析对象
JWTVerifier jwtVerifier=JWT.require(Algorithm.HMAC256(SECRET)).build();
//验证JWT
DecodedJWT decodedJWT = jwtVerifier.verify(token);
Claim username = decodedJWT.getClaim("username");
return username.asString();
}catch (TokenExpiredException e){
e.printStackTrace();
}
return null;
}
/**
* 获取JWT里面相前权限
*/
public List<String> getAuth(String token){
try{
// 使用秘钥创建一个解析对象
JWTVerifier jwtVerifier=JWT.require(Algorithm.HMAC256(SECRET)).build();
//验证JWT
DecodedJWT decodedJWT = jwtVerifier.verify(token);
Claim auth = decodedJWT.getClaim("auth");
return auth.asList(String.class);
}catch (TokenExpiredException e){
e.printStackTrace();
}
return null;
}
}
3、写主类测试一下
package com.powernode.test;
import com.powernode.util.JwtUtils;
import java.util.Arrays;
import java.util.List;
public class JwtTest {
public static void main(String[] args) {
JwtUtils jwtUtils = new JwtUtils();
List<String> authList = Arrays.asList("student:add", "student:update", "student:delete");
String jwtToken = jwtUtils.createJwt(10, "obama", authList);
System.out.println(jwtToken);
boolean verifyResult = jwtUtils.verifyJwtToken(jwtToken);
System.out.println(verifyResult);
if(verifyResult){
Integer userId = jwtUtils.getUserId(jwtToken);
String userName = jwtUtils.getUserName(jwtToken);
List<String> userAuthList = jwtUtils.getUserAuth(jwtToken);
System.out.println("userId: "+userId);
System.out.println("userName: "+userName);
System.out.println("authList: "+userAuthList);
}
}
}
总结
JWT就是一个加密的带用户信息的字符串,没学习JWT之前,我们在项目中都是返回一个基本的字符串,然后请求时带上这个字符串,再从session或者redis中(共享session)获取当前用户,学过JWT以后我们可以把用户信息直接放在jwt字符串中返回给前端,然后用户请求时带过来,我们是在服务器进行解析拿到当前用户,这就是两种登录方式,这两种方式有各自的优缺点。
结合JWT
JWT+Spring Security+redis+mysql 实现认证,在数据库工程的基础上添加数据
redis主要起退出功能的作用
1、添加依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.powernode</groupId>
<artifactId>springsecurity-16-jwt-authentication-redis</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.13</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<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.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</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>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2、添加application.xml配置
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123
url: jdbc:mysql://127.0.0.1:3306/security_study?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
redis:
host: 192.168.75.131
port: 6379
database: 0
password: 123456
mybatis:
type-aliases-package: com.powernode.entity # 配置类型别名包
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # sql日志在控制台上输出
map-underscore-to-camel-case: true # 支持下划线转驼峰
mapper-locations: classpath:mapper/*.xml # 映射文件位置
3、web配置类
@Slf4j
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private JwtVerifyFilter jwtVerifyFilter;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private JwtUtils jwtUtils;
@Resource
private ObjectMapper objectMapper;
@Override
protected void configure(HttpSecurity http) throws Exception {
//插入到用户名密码认证过滤器之前
http.addFilterBefore(jwtVerifyFilter, UsernamePasswordAuthenticationFilter.class);
//不需要session了。
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.authorizeRequests()
.anyRequest().authenticated();
//禁用跨域请求保护
http.csrf().disable();
http.formLogin().successHandler( //认证成功处理器
(request,response,authentication)->{
//从认证信息中拿到用户详情
SecurityUserDetails securityUserDetails= (SecurityUserDetails) authentication.getPrincipal();
SysUser sysUser = securityUserDetails.getSysUser();
sysUser.setPassword(null); //擦除密码
//转成json对象
String jsonSysUser = objectMapper.writeValueAsString(sysUser);
//获取用户权限
List<GrantedAuthority> authorities = (List<GrantedAuthority>) securityUserDetails.getAuthorities();
//使用stream
List<String> authList = authorities.stream() //获取流
.map(GrantedAuthority::getAuthority) // 映射(集合里一个一个映射) 调用GrantedAuthority 的getAuthority
.collect(Collectors.toList()); //收集
//创建jwt
String jwtToken = jwtUtils.createJwt(jsonSysUser, authList);
//将jwt存储到redis
stringRedisTemplate.opsForValue().set("logintoken:"+jwtToken,authentication.toString(),2, TimeUnit.HOURS);
//声明响应给前端的对象
HttpResult httpResult=HttpResult.builder()
.code(200)
.data(jwtToken)
.build();
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.println(objectMapper.writeValueAsString(httpResult));
writer.flush();
writer.close();
}
);
http.logout().logoutSuccessHandler( //退出成功的时候,从redis中删除token
(request, response,authentication)->{
String authHeader = request.getHeader("Authorization");
if(!StringUtils.hasText(authHeader)){
HttpResult httpResult=HttpResult.builder()
.code(-1)
.msg("没有传token")
.build();
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.println(objectMapper.writeValueAsString(httpResult));
writer.flush();
writer.close();
return;
}
String jwtToken = authHeader.replace("Bearer ", "");
boolean verifyJwtToken = jwtUtils.verifyJwtToken(jwtToken);
if(verifyJwtToken){
stringRedisTemplate.delete("logintoken:"+jwtToken);
HttpResult httpResult=HttpResult.builder()
.code(200)
.msg("退出成功")
.build();
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.println(objectMapper.writeValueAsString(httpResult));
writer.flush();
writer.close();
return;
}
}
);
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
4、JWT过滤类
@Component
@Slf4j
public class JwtVerifyFilter 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 authorizationHeader = request.getHeader("Authorization");
if(!StringUtils.hasText(authorizationHeader)){
HttpResult httpResult=HttpResult.builder()
.code(-1)
.msg("您没有传输jwt")
.build();
printFront(response, httpResult);
return;
}
//获取jwt
String jwtToken = authorizationHeader.replace("Bearer ", "");
//校验jwt
boolean verifyResult = jwtUtils.verifyJwtToken(jwtToken);
if(!verifyResult){
HttpResult httpResult=HttpResult.builder()
.code(-1)
.msg("jwt 不合法")
.build();
printFront(response, httpResult);
return;
}
Boolean isExists = stringRedisTemplate.hasKey("logintoken:" + jwtToken);
if(!isExists){
HttpResult httpResult=HttpResult.builder()
.code(-1)
.msg("用户已退出")
.build();
printFront(response, httpResult);
return;
}
//从jwt的payload中获取用户信息和用户权限
String userInfo = jwtUtils.getUserInfo(jwtToken);
List<String> authList = jwtUtils.getUserAuth(jwtToken);
List<SimpleGrantedAuthority> authorityList = authList.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
//将json字符串反序列化成Sysuser对象
SysUser sysUser = objectMapper.readValue(userInfo, SysUser.class);
SecurityUserDetails securityUserDetails=new SecurityUserDetails(sysUser);
//下面三句是复杂一点
SecurityContext securityContext = SecurityContextHolder.getContext();
//实现UserDetails对象的就是principle,密码为空,权限放进去
UsernamePasswordAuthenticationToken authenticationToken=new UsernamePasswordAuthenticationToken(securityUserDetails,null,authorityList);
//把认证信息放到安全上下文
securityContext.setAuthentication(authenticationToken);
//继续过滤器链
this.doFilter(request,response,filterChain);
}
private void printFront(HttpServletResponse response, HttpResult httpResult) throws IOException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.println(objectMapper.writeValueAsString(httpResult));
writer.flush();
writer.close();
}
/**
* 不过滤登录请求,别的需要走此过滤器
* @param request
* @return
* @throws ServletException
*/
@Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
String requestURI = request.getRequestURI();
return requestURI.equals("/login");
}
}
5、UserDetailsService
@Service
@Slf4j
public class UserDetailsServiceImpl 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> userMenuList = sysMenuDao.queryMenuByUserId(sysUser.getUserId());
//将一个字符串列表转换成一个GrantedAuthority列表
//stream流的书写
List<GrantedAuthority> grantedAuthorityList = userMenuList.stream() //获取集合的流
.map(SimpleGrantedAuthority::new) //映射 SimpleGrantedAuthority::new 创建对象
.collect(Collectors.toList());//收集 成list
SecurityUserDetails securityUserDetails = new SecurityUserDetails(sysUser);
securityUserDetails.setGrantedAuthorityList(grantedAuthorityList);
return securityUserDetails;
}
}
6、JWTUtils
@Component
@Slf4j
public class JwtUtils {
public static final String SECRET_KEY="THOMASKEY";
/**
* 创建jwt
* @param userInfo
* @param authList
* @return
*/
public String createJwt(String userInfo, List<String> authList){
Date currentTime = new Date();
Date expireTime=new Date(currentTime.getTime()+1000*60*60*2); //过期时间
Map<String, Object> headerClaims =new HashMap<>();
headerClaims.put("alg","HS256");
headerClaims.put("typ","JWT");
String jwtToken = JWT.create().withHeader(headerClaims) //头部
.withIssuer("thomas") //创建者
.withIssuedAt(currentTime) //创建时间
.withExpiresAt(expireTime) //过期时间为2小时
.withClaim("userInfo", userInfo) //用户信息
.withClaim("authList", authList)
.sign(Algorithm.HMAC256(SECRET_KEY));//签名
return jwtToken;
}
/**
* 校验jwt
* @param jwtToken
* @return
*/
public boolean verifyJwtToken(String jwtToken){
//创建校验器
JWTVerifier jwtVerifier=JWT.require(Algorithm.HMAC256(SECRET_KEY)).build();
//校验
try {
DecodedJWT decodedJWT = jwtVerifier.verify(jwtToken);
return true;
} catch (JWTVerificationException e) {
System.out.println("jwt 是非法的");
return false;
}
}
/**
* 获取用户信息
* @param jwtToken
* @return
*/
public String getUserInfo(String jwtToken){
try {
//创建校验器
JWTVerifier jwtVerifier=JWT.require(Algorithm.HMAC256(SECRET_KEY)).build();
DecodedJWT decodedJWT = jwtVerifier.verify(jwtToken);
return decodedJWT.getClaim("userInfo").asString();
} catch (Exception e) {
return null;
}
}
/**
* 获取用户拥有的权限
* @param jwtToken
* @return
*/
public List<String> getUserAuth(String jwtToken){
try {
//创建校验器
JWTVerifier jwtVerifier=JWT.require(Algorithm.HMAC256(SECRET_KEY)).build();
DecodedJWT decodedJWT = jwtVerifier.verify(jwtToken);
return decodedJWT.getClaim("authList").asList(String.class);
} catch (Exception e) {
return null;
}
}
}
更多推荐


所有评论(0)