文章目录
Spring Security
对于权限的管理,在企业应用程序的开发中,是必不可少的功能,但是能够灵活且强大的权限控制又不是一件容易的事情,所以在自己学习编写权限控制体系的基础上也接触一下成熟的框架,Spring 的全家桶系列 Spring Security 就进入了我们的视线。
1、概念
Spring Security,这是一种基于 Spring AOP 和 Servlet 过滤器的安全框架。它提供全面的安全性解决方案,同时在 Web 请求级和方法调用级处理身份确认和授权。
认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是那个用户。
授权:经过认证后判断当前用户是否有权限进行某个操作。
认证和授权也是 Spring Security 作为安全框架的核心功能
搭建如图项目:
2、入门案例
我们通过一个简单的入门案例来了解 Spring Security 。
引入依赖
我们已父子项目方式搭建,所有的依赖都在父项目中书写,子项目只需继承父项目即可。父项目中的写法:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.0</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
控制类
书写一个普通的 controller 类
@RestController
@RequestMapping("/first")
public class FirstController {
@GetMapping("/a")
public JsonResult a() {
return ResultTool.success("Success");
}
}
启动类
@SpringBootApplication
public class Security01Application {
public static void main(String[] args) {
SpringApplication.run(Security01Application.class, args);
}
}
运行效果
当我们还按照过去的方式访问 http://localhost:8080路径时,发现弹出了登录框,这个是 Spring Security 自带的登录页面,要求我们必须登录后才能访问系统资源。
并且在项目启动后,在控制台能看到初始生成的密码。
使用 Spring Security 后访问系统任意资源时,就会跳转到默认登录页面,默认账号是 user ,登录成功后,才能访问才能对目标接口进行访问。以后只要不关闭浏览器或者服务器,都可以直接访问。也可以手动登出,路径是:logout 。
3、登陆认证
我们刚才通过一个简单的案例,了解了 Spring Security 的基本概念。也发现 Spring Security 会在服务器启动时随机生成密码,那么有的童鞋就会想到,能不能自己去定义这个密码,甚至于使用数据库来校验用户登陆。
自定义账号密码
这种方式其实很简单,只需要在配置文件 (application.yml) 中设置账号密码就行。
spring:
security:
user:
name: root
password: root
还可以在配置类中进行设置,注释上边的写法,在 config 包下创建一个配置类 SecurityConfig 来配置账号密码信息。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
PasswordEncoder passwordEncoder = getPasswordEncoder();
String pass = passwordEncoder.encode("123456");
auth.inMemoryAuthentication().withUser("lisi").password(pass).roles("admin");
}
@Bean
PasswordEncoder getPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
BCryptPasswordEncoder 类是 Spring Security 中的一个加密方法类。BCryptPasswordEncoder 方法采用了 SHA-256+随机盐+密钥对密码进行加密。 SHA 是一种安全 Hash 函数(SHA),是使用最广泛的Hash函数。
加密算法与 hash 算法的区别:加密算法是可逆的,加密算法的基本过程是对原来为明文的数据按某种算法进行处理,使其成为不可读的一段代码为“密文”,但在用相应的密钥进行操作之后就可以得到原来的内容。 hash 算法是一种单向密码体制,即它是一个从明文到密文的不可逆的映射,只有加密过程,没有解密过程。同时,哈希函数可以将任意长度的输入经过变化以后得到固定长度的输出。
数据库校验方式
前面两种都是写死的,可能对于固定的超级管理员可以用,我们真实的项目场景肯定都是数据库里面的。
需要自定义查询,我们先把SecurityConfig 注释了,重新创建一个类 SpringSecurityConfig 来从数据库中判断账号密码。
引入数据库相关依赖
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.9</version>
</dependency>
application.yml
spring:
datasource:
druid:
url: jdbc:mysql://localhost:3306/zgh
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case: false
书写 bean 包的类
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements Serializable {
private Integer id;
private String name;
private String password;
private String nickname;
private String gender;
private String birthday;
}
书写 mapper 包的类
@Mapper
public interface UsersMapper extends BaseMapper<Users> {
}
书写一个 UserServiceImpl 类继承ServiceImpl
@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService, UserDetailsService {
@Resource
private UserMapper mapper;
@Resource
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.info("开始进行登陆校验,账号是:{}", username);
//检验账号是否存在
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("name", username);
User user = mapper.selectOne(wrapper);
if (user == null) {
log.info("账号不存在");
throw new UsernameNotFoundException("账号不存在");
}
List<GrantedAuthority> auths = AuthorityUtils.createAuthorityList("admin");
log.info("校验通过");
// 发送到下一级
return new org.springframework.security.core.userdetails.User(username, passwordEncoder.encode(user.getPassword()), auths);
}
}
书写 SpringSecurityConfig 配置类
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private UserDetailsService userDetailsService;
@Resource
private PasswordEncoder passwordEncoder;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
/*
String pass = passwordEncoder.encode("123456");
auth.inMemoryAuthentication().withUser("lisi").password(pass).roles("admin");*/
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
}
}
输入错误的账户信息
查看控制台:
这时候继续访问刚才的页面,输入数据库的账号和密码
点击登录,跳转页面
查看控制台
流程:当输入 localhost:8080/first/a,敲回车的时候,路径变成 localhost:8080/login,判断你没有登录,进入到login的请求,这是login是controller的一个地址,自己没写controller,它把controller写了,它写了controller之后,再输入账号和密码之后,登录按钮一按,账号和密码就会送到login的controller中去,拿到账号密码之后,把账号给了(调用UserService的实现类中的loadUserByUsername()方法来验证账户是否存在,和密码没有关系),如果验证通过了,就会把账号、加密后的密码、权限,一起封装为一个对象(org.springframework.security.core.userdetails.User(username, passwordEncoder.encode(user.getPassword()), auths);),把这个对象交给下一级,验证密码和数据库中的密码是否相匹配,判断是否通过。
4、自定义登录页面
前后端分离:不一定不是在一个项目中(可在可不在)
以前:jsp页面去请求controller,controller得到数据之后利用response跳转到页面。
分离更多指的是这种情况:Ajax操作,比如前端向后端发送了一个请求,前端项目和后端可在、可不在(得跨域)同一个项目中,通过url网址的方式向后端发送请求,后端接收请求,不能通过 response方式或者转发的方式把前端的页面实现跳转,只能返回给前端一个
成功或者失败的数据,这是显得前端和后端没有关系了,都是借用第三者json来完成数据的交互,这个才叫分离。
我们刚才一直使用 security 自带的登陆页面,但是在实际使用中我们更多的是使用我们自己书写的登陆页面,要想设置其实很简单,如下几步就行。
创建登录页面
<!--
表单提交用户信息,注意
1.账号和密码的名字
2.action的提交地址和配置类中设置一致
-->
<form action="user/login" method="post">
账号:<input type="text" name="username"/><br/>
密码:<input type="password" name="password"/><br/>
<button>登录</button>
</form>
创建首页页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
我是首页!
</body>
</html>
在配置类(SecurityConfig)中设置登录页面
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()//自定义登陆页面
.loginPage("/login.html") //登录页面设置(指定登录名)
.loginProcessingUrl("/user/login")//登陆访问路径 这里同 action
.defaultSuccessUrl("/index.html").permitAll()//登陆成功之后,跳转路径
.and().authorizeRequests()//设置请求的权限
//设置哪些路径可以直接访问,不需要认证(登录)
.antMatchers("/login.html", "/user/login").permitAll()
.anyRequest().authenticated() //所有请求都可以访问
.and().csrf().disable(); //关闭csrf
}
运行结果:
1、启动项目,进行登录
2、去到login页面,login没有请求,返回到上一界面
3、再次点击登录
5、授权管理
在项目中,有很多接口是针对不同角色权限的,如果角色是超级管理员,就拥有访问所有权限的能力,如果不是超级管理员,访问其他接口是不能允许的。 有些用户具有部分权限,就可以访问这些权限所能访问的内容,如果要实现这种效果,我们就需要授权管理。
基于权限
写几个页面
<!DOCTYPE html>
<html lang="en">
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>1.html</h1>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>2.html</h1>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>3.html</h1>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>4.html</h1>
</body>
</html>
**hasAuthority()**方法:如果当前的主体具有指定的权限,则返回 true,否则返回 false。
配置类
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()//自定义登陆页面
.loginPage("/login.html") //登录页面设置(指定登录名)
.loginProcessingUrl("/user/login")//登陆访问路径 这里同 action
.defaultSuccessUrl("/index.html").permitAll()//登陆成功之后,跳转路径
.and().authorizeRequests()//设置请求的权限
.antMatchers("/login.html", "/user/login").permitAll()//设置哪些路径可以直接访问,不需要认证(登录)
// 设置当前登录用户访问html页面时需要manager1、manager2、manager3或manager4权限
.antMatchers("/1.html").hasAuthority("manager1")
.antMatchers("/2.html").hasAnyAuthority("manager1", "manager2", "manager3")
.antMatchers("/3.html").hasAuthority("manager2")
.antMatchers("/4.html").hasAnyAuthority("manager1", "manager2")
.anyRequest().authenticated() //所有请求都可以访问
.and().csrf().disable(); //关闭csrf
}
授予权限
UserServiceImpl——>loadUserByUsername
// 获取权限 假的(后续会讲解权限问题)
List<GrantedAuthority> auths = AuthorityUtils.createAuthorityList("manager1", "manager2");
运行结果:
访问 html 页面需要权限
访问1.html 只要有 manager1
访问2.html 只要有manager1、manager2、manager3 权限之一
访问3.html 只要有 manager2
访问4.html 只要有manager1、manager2权限之一
基于角色
配置类
//.antMatchers("/3.html").hasAuthority("manager2")
//.antMatchers("/4.html").hasAnyAuthority("manager1", "manager2")
.antMatchers("/3.html").hasRole("man1")
.antMatchers("/4.html").hasAnyRole("man3", "man2")
业务类
// 获取权限 假的(后续会讲解权限问题)
List<GrantedAuthority> auths = AuthorityUtils.createAuthorityList("manager1", "manager2","ROLE_man1");
运行结果
访问3.html需要man1角色,而设置中有man1 角色,所以可以访问
访问4.html需要man2或man3角色,而设置中没有一个 对应的角色,所以不可以访问
配置 403 页面
当没有权限的时候访问会报403,我们可以自定义一个页面。
创建一个 403.html 页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>403</title>
</head>
<body>
<h2>你没有相应的权限访问这个页面!</h2>
</body>
</html>
配置类中设置
.and().exceptionHandling().accessDeniedPage("/403.html")
运行结果
访问没有权限的4.html
由此可见,设置成功
用户注销
SpringSecurity 的注销功能很简单,只需要一个超链接地址为 /logout 就行。
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<!--我是首页!-->
我是注销页面!
<a href="/logout">注销</a>
</body>
</html>
运行结果:
1、登录
2、注销
3、注销之后返回页面,重新登录
6、分离项目
通过刚才的案例我们了解到了 SpringSecurity 认证授权的效果。可是刚才通过的是表单方式进行提交,在其后的项目中我们可能更多的使用前后端分离效果,那么我们就要返回给前端对应的消息来告诉前端认证授权的情况。
前期准备
搭建项目
导入依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.9</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.32</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring_boot_commons</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
application.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/zgh
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case: false
数据库信息
前后端分离项目,需要后端返回消息来标注相应状态,这时候我们创建三个类来对返回的消息统一格式。
统一返回格式
@Data
public class JsonResult<T> implements Serializable {
private Boolean success;
private Integer errorCode;
private String errorMsg;
private T data;
public JsonResult() {
}
public JsonResult(boolean success) {
this.success = success;
this.errorCode = success ? ResultCode.SUCCESS.getCode() : ResultCode.COMMON_FAIL.getCode(
this.errorMsg = success ? ResultCode.SUCCESS.getMessage() : ResultCode.COMMON_FAIL.getMessage();
}
public JsonResult(boolean success, ResultCode resultEnum) {
this.success = success;
this.errorCode = success ? ResultCode.SUCCESS.getCode() : (resultEnum == null ? ResultCode.COMMON_FAIL.getCode() : resultEnum.getCode());
this.errorMsg = success ? ResultCode.SUCCESS.getMessage() : (resultEnum == null ? ResultCode.COMMON_FAIL.getMessage() : resultEnum.getMessage());
}
public JsonResult(boolean success, T data) {
this.success = success;
this.errorCode = success ? ResultCode.SUCCESS.getCode() : ResultCode.COMMON_FAIL.getCode();
this.errorMsg = success ? ResultCode.SUCCESS.getMessage() : ResultCode.COMMON_FAIL.getMessage();
this.data = data;
}
public JsonResult(boolean success, ResultCode resultEnum, T data) {
this.success = success;
this.errorCode = success ? ResultCode.SUCCESS.getCode() : (resultEnum == null ? ResultCode.COMMON_FAIL.getCode() : resultEnum.getCode());
this.errorMsg = success ? ResultCode.SUCCESS.getMessage() : (resultEnum == null ? ResultCode.COMMON_FAIL.getMessage() : resultEnum.getMessage());
this.data = data;
}
}
返回状态码定义
/**
* @Description: 返回码定义
* 规定:
* #1表示成功
* #1001~1999 区间表示参数错误
* #2001~2999 区间表示用户错误
* #3001~3999 区间表示接口异常
*/
public enum ResultCode {
/* 成功 */
SUCCESS(200, "成功"),
/* 默认失败 */
COMMON_FAIL(999, "失败"),
/* 参数错误:1000~1999 */
PARAM_NOT_VALID(1001, "参数无效"),
PARAM_IS_BLANK(1002, "参数为空"),
PARAM_TYPE_ERROR(1003, "参数类型错误"),
PARAM_NOT_COMPLETE(1004, "参数缺失"),
/* 用户错误 */
USER_NOT_LOGIN(2001, "用户未登录"),
USER_ACCOUNT_EXPIRED(2002, "账号已过期"),
USER_CREDENTIALS_ERROR(2003, "密码错误"),
USER_CREDENTIALS_EXPIRED(2004, "密码过期"),
USER_ACCOUNT_DISABLE(2005, "账号不可用"),
USER_ACCOUNT_LOCKED(2006, "账号被锁定"),
USER_ACCOUNT_NOT_EXIST(2007, "账号不存在"),
USER_ACCOUNT_ALREADY_EXIST(2008, "账号已存在"),
USER_ACCOUNT_USE_BY_OTHERS(2009, "账号下线"),
/* 业务错误 */
NO_PERMISSION(3001, "没有权限");
private Integer code;
private String message;
ResultCode(Integer code, String message) {
this.code = code;
this.message = message;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
/**
* 根据code获取message
* @return
*/
public static String getMessageByCode(Integer code) {
for (ResultCode ele : values()) {
if (ele.getCode().equals(code)) {
return ele.getMessage();
}
}
return null;
}
}
handler包
未登录效果
当用户未登录时,会自动进入当前类的 commence 方法,我们在这个方法中返回 JSON 格式的错误信息。
// 用户没有登陆时执行的代码
@Component
@Slf4j
public class NotLoginAuthentication implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
throws IOException, ServletException {
log.info("用户没有登陆");
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.println(JSONArray.toJSONString(ResultTool.fail("用户没有登陆")));
}
}
登录失败效果
当用户登录失败时(不论是账号未找到,密码错误还是权限问题),都会进入这个类的指定方式,我们在这个方法中返回 JSON 格式的错误信息。
//登录失败的操作
@Component
@Slf4j
public class FailureAuthenticationHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception)
throws IOException, ServletException {
log.info("账号和密码错误!");
response.setContentType("application/json;charset=utf-8");
PrintWriter out=response.getWriter();
out.println(JSONArray.toJSONString(ResultTool.fail("账号或者密码错误")));
}
}
登录成功效果
同样的道理,对正确的消息返回 JSON 格式信息。
// 登陆成功后要执行的代码
@Component
@Slf4j
public class SuccessAuthenticationHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
log.info("登陆成功");
// 1. 生成jwt字符串
// 2. 保存到redis
// 3. 返回给前端token
//处理编码方式,防止中文乱码的情况
response.setContentType("application/json;charset=utf-8");
//塞到response中返回给前台
PrintWriter out = response.getWriter();
out.println(JSONArray.toJSONString(ResultTool.success("登陆成功!")));
}
}
配置文件
在配置文件中注册三个效果。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private NotLoginAuthentication authentication;
@Resource
private FailureAuthenticationHandler failureAuthenticationHandler;
@Resource
private SuccessAuthenticationHandler successAuthenticationHandler;
@Override
//模拟数据(前后端分离)
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
String pass = getPasswordEncoder().encode("123456");
auth.inMemoryAuthentication().withUser("admin").password(pass).roles(("manager"));
}
//前后端分离
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.permitAll()
.successHandler(successAuthenticationHandler) //登录成功处理逻辑
.failureHandler((failureAuthenticationHandler)) //登录失败处理逻辑
//登录成功处理逻辑
.and()
.exceptionHandling().authenticationEntryPoint(authentication)//匿名用户访问无权限资源时的异常处理
.and()
.authorizeRequests()
.antMatchers("/login.html", "/login", "/index.html", "/").permitAll()
.antMatchers("/1.html").hasAnyAuthority("admin")
.antMatchers("/2.html").hasAnyAuthority("manager")
.anyRequest().authenticated() //所有请求都可以访问
.and().csrf().disable(); //关闭csrf
}
@Bean
PasswordEncoder getPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
启动类
@SpringBootApplication
public class SpringBootSecurity2Application {
public static void main(String[] args) {
SpringApplication.run(SpringBootSecurity2Application.class, args);
}
}
前端部分
1.html 2.html index.html login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>1.html</h1>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>2.html</h1>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>index.html</h1>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<!--
表单提交用户信息,注意
1.账号和密码的名字
2.action的提交地址和配置类中设置一致
-->
<form action="/login" method="post">
账号:<input type="text" name="username"/><br/>
密码:<input type="password" name="password"/><br/>
<button>登录</button>
</form>
</body>
</html>
测试结果
登录成功的情况
测试login.html(登录之后不保存登录信息,访问其他页面还是认为没有登录)
页面
后端控制台
登录失败的情况
没有登录就访问其他资源情况
测试index.html(有权限)
测试1.html(无权限)
测试2.html(无权限)
7、进一步拓展
刚才的例子中,我们可以通过几个 Handler 操作未登录时、登录失败和登录成功情况。那么前后端分离情况下,如何保存用户登录状态呢?下边我们通过 Security 的几个过滤器来实现这个功能。
前期准备
其他模块操作
1、将jwt依赖导入公共模块
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- jaxb依赖包 -->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-core</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>
注意:版本号不要修改,以免出现错误
2、给公共模块创建config包
这里通过 JWT 令牌方式来验证用户登录,这里创建 JwtConfig 类。
package zgh.config;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import zgh.bean.User;
import java.util.Date;
public class JwtConfig {
//常量
public static final long EXPIRE = 1000 * 60 * 60 * 24; //token过期时间
public static final String APP_SECRET = "1234"; //秘钥,加盐
// @param id 当前用户ID
// @param issuer 该JWT的签发者,是否使用是可选的
// @param subject 该JWT所面向的用户,是否使用是可选的
// @param ttlMillis 什么时候过期,这里是一个Unix时间戳,是否使用是可选的
// @param audience 接收该JWT的一方,是否使用是可选的
// 生成json token字符串的方法
public static String getJwtToken(User user) {
String jwtToken = Jwts.builder()
.setHeaderParam("typ", "JWT") //头部信息
.setHeaderParam("alg", "HS256") //头部信息
//下面这部分是payload部分
// 设置默认标签
.setSubject("dailyblue") //设置jwt所面向的用户
.setIssuedAt(new Date()) //设置签证生效的时间
.setExpiration(new Date(System.currentTimeMillis() + EXPIRE)) //设置签证失效的时间
//自定义的信息,这里存储id和姓名信息
.claim("id", user.getId()) //设置token主体部分 ,存储用户信息
.claim("name", user.getName())
.claim("nickName", user.getNickname())
//下面是第三部分
.signWith(SignatureAlgorithm.HS256, APP_SECRET)
.compact();
// 生成的字符串就是jwt信息,这个通常要返回出去
return jwtToken;
}
/**
* 判断token是否存在与有效
* 直接判断字符串形式的jwt字符串
*
* @param jwtToken
* @return
*/
public static boolean checkToken(String jwtToken) {
if (null == jwtToken) return false;
try {
Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
/**
* 解析JWT
*
* @param jwt
* @return
*/
public static Claims parseJWT(String jwt) {
Claims claims = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwt).getBody();
return claims;
}
}
本模块操作
搭建项目
导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
SecurityUser 类,这个类描述用户信息和它的权限信息。
@Data
public class SecurityUser implements UserDetails {
// 登录用户的基本信息
private User user;
//当前权限
private List<String> authentications;
//获取权限
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
authentications.forEach(permission -> {
if (!StringUtils.isEmpty(permission)) {
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permission);
authorities.add(authority);
}
});
return authorities;
}
//得到密码
@Override
public String getPassword() {
return user.getPassword();
}
//得到用户名
@Override
public String getUsername() {
return user.getName();
}
//权限是否可用
@Override
public boolean isAccountNonExpired() {
return true;
}
//账号是否被锁定
@Override
public boolean isAccountNonLocked() {
return true;
}
//账号是否过期
@Override
public boolean isCredentialsNonExpired() {
return true;
}
//账号是否可见
@Override
public boolean isEnabled() {
return true;
}
}
mapper包
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
service包
UsersService 类,业务类,负责调用 Mapper 的方法
public interface UserService extends IService<User> {
}
实现类(impl——>UserServiceImpl)
@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserDetailsService, UserService {
@Resource
private UserMapper mapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.info("开始进入service实现登陆操作");
// 验证账号是否正确
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("name", username);
User user = mapper.selectOne(wrapper);
if (StringUtils.isEmpty(user)) {
log.info("账号不存在");
throw new UsernameNotFoundException("账号没有找到!");
}
user.setPassword(new BCryptPasswordEncoder().encode(user.getPassword()));
log.info("数据库密码是:{}", user.getPassword());
// 如果正确,获取权限 假的
List<String> auths = Arrays.asList("manager1", "manager2", "manager3");
// 包装UserDetails类并返回
SecurityUser securityUser = new SecurityUser();
securityUser.setUser(user);
securityUser.setAuthentications(auths);
log.info("账号验证通过,开始验证密码!");
return securityUser;
}
}
登录过滤器
这个是一个 Filter ,不需要 Spring 来注入
- attemptAuthentication 方法 用户登录时触发,获取账号和密码,传递到我们自己书写的 UsersService 中。
- successfulAuthentication 方法 登录成功后执行的方法,一般存放 token 到 Redis 中,返回成功信息。
- unsuccessfulAuthentication 方法 登录失败后执行的方法。
后两个如果书写了,上一个案例中的那两个 Handler 就可以不书写了。
//登录过滤器
@Slf4j
public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
private StringRedisTemplate stringRedisTemplate;
private PasswordEncoder passwordEncoder;
public JwtLoginFilter(AuthenticationManager authenticationManager, StringRedisTemplate stringRedisTemplate, PasswordEncoder passwordEncoder) {
this.authenticationManager = authenticationManager;
this.stringRedisTemplate = stringRedisTemplate;
this.passwordEncoder = passwordEncoder;
// 关闭登录只允许 post
this.setPostOnly(false);
// 设置登陆路径,并且post请求
this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/login", "POST"));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
log.info("开始用户登陆操作");
String username = request.getParameter("username");
String password = request.getParameter("password");
String encoderPassword = passwordEncoder.encode(password);
log.info("账号:{},密码:{},加密后密码:{}", username, password, encoderPassword);
return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
log.info("登陆成功");
// 1. 生成jwt字符串
SecurityUser user = (SecurityUser) authResult.getPrincipal(); // 获取到登陆成功后保存的user
String token = JwtConfig.getJwtToken(user.getUser());
// 2. 保存到redis
stringRedisTemplate.opsForValue().set("TOKEN:" + user.getUser().getId(), token, 1, TimeUnit.DAYS);
// 3. 返回给前端token
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.println(JSONArray.toJSONString(ResultTool.success(token)));
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
log.info("账号或者密码错误");
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.println(JSONArray.toJSONString(ResultTool.fail("账号或者密码错误")));
}
}
验证过滤器
这个过滤器会在每次请求(不需要触发的可以在配置文件中设置)时去触发,主要作用是验证用户是否登录。
// 校验用户是否登陆的过滤器
@Slf4j
public class TokenAuthFilter extends BasicAuthenticationFilter {
private StringRedisTemplate stringRedisTemplate;
public TokenAuthFilter(AuthenticationManager authenticationManager, StringRedisTemplate stringRedisTemplate) {
super(authenticationManager);
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("开始校验用户是否登陆");
// 1. 获取到前端发送过来的token
String token = request.getHeader("token");
log.info("前端发送过来的token:{}", token);
// 2. 校验token
// 2.1 是否发送
if (token != null) {
// 2.2 是否合法
boolean flag = JwtConfig.checkToken(token);
log.info("是否合法:{}", flag);
if (flag) {
Claims claims = JwtConfig.parseJWT(token);
String id = claims.get("id").toString();
String redisToken = stringRedisTemplate.opsForValue().get("TOKEN:" + id);
log.info("redis的token:{}", redisToken);
// 2.3 是否篡改
if (token.equals(redisToken)) {
// 3. 获取权限
List<String> list = Arrays.asList("manager1", "manager2", "manager3");
List<GrantedAuthority> auths = new ArrayList<>();
list.forEach((e) -> {
GrantedAuthority authority = new SimpleGrantedAuthority(e);
auths.add(authority);
});
log.info("成功,下放权限!");
// 4. 下放权限
UsernamePasswordAuthenticationToken upat = new UsernamePasswordAuthenticationToken(id, token, auths);
// 有权限,则放入权限上下文中
SecurityContextHolder.getContext().setAuthentication(upat);
}
}
} else {
// 清空操作
SecurityContextHolder.getContext().setAuthentication(null);
}
chain.doFilter(request, response);
}
}
配置文件
注释掉前面的config的前后端分离的部分
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private NotLoginAuthentication authenticationEntryPoint;
@Resource
private FailureAuthenticationHandler failureHandler;
@Resource
private SuccessAuthenticationHandler successHandler;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private UserDetailsService service;
// 前后端分离
/*@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
String pass = getPasswordEncoder().encode("123456");
auth.inMemoryAuthentication().withUser("admin").password(pass).roles("manager");
}*/
// 前后端分离
/*@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.permitAll()
.successHandler(successHandler) //登录成功处理逻辑
.failureHandler(failureHandler) //登录失败处理逻辑
//异常处理(权限拒绝、登录失效等)
.and()
.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint)//匿名用户访问无权限资源时的异常处理
.and()
.authorizeRequests()
.antMatchers("/login.html", "/login", "/index.html", "/").permitAll()
.antMatchers("/1.html").hasAnyAuthority("admin")
.antMatchers("/2.html").hasAnyAuthority("manager")
.anyRequest().authenticated() //所有请求都可以访问
.and().csrf().disable(); //关闭csrf
}*/
// 进一步拓展
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint) // 未登录 handler
.and().csrf().disable() // 关闭 csrf 跨域请求
.cors().and() // security允许跨域
.formLogin()
.loginProcessingUrl("/login") // 设定登录请求接口
.usernameParameter("username")
.passwordParameter("password")
.permitAll()
.and()
.authorizeRequests() // 请求设置
.antMatchers("/login").permitAll() // 配置不需要认证的接口
.anyRequest().authenticated() // 任何请求都需要认证
.and()
.addFilter(new TokenAuthFilter(authenticationManager(), stringRedisTemplate)) // 认证交给 自定义 TokenLoginFilter 实现
.addFilter(new JwtLoginFilter(authenticationManager(), stringRedisTemplate, getPasswordEncoder()))
.httpBasic();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(service).passwordEncoder(getPasswordEncoder());
}
@Bean
PasswordEncoder getPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
测试结果
APiPost测试:
控制台输出结果
输入正确的用户信息和密码
带上controller测试
controller包
@Slf4j
@RestController
@RequestMapping("/first")
public class FirstController {
@GetMapping("/a")
public JsonResult a() {
log.info("访问了a");
return ResultTool.success();
}
@GetMapping("/b")
public JsonResult b() {
log.info("访问了b");
return ResultTool.success();
}
}
携带token的结果
去掉token的结果
关于记录的问题
注意:前端发送过来的token:null时,需要去清空凭证。
在一般的应用项目中,用户只是面对页面,无法去掉token,一般在点击登出操作后,会清空记录,不用担心此问题。
业务流程
有两个过滤器,JwtLoginFilter(登录)和TokenAuthFilter(验证)
JwtLoginFilter的作用是:当用户实现登录操作时候,输入账号和密码时,JwtLoginFilter先进行拦截,得到账号和密码之后,包装成为一个凭证,然后传递下去,进入service层,验证用户的账号是否存在,如果账号存在,把用户和权限包装起来,再生成一个凭证,发送给security,security来进行密码的校验;如果错误,直接返回错误信息(账号没有找到)。当账号和密码认证通过了,继续进入到JwtLoginFilter的successfulAuthentication方法中;当密码校验失败了,会进入wtLoginFilter的unsuccessfulAuthentication方法中。
总结:
登录时,先进filter,filter传给service,service传给security,security会把最终的结果再传给filter,filter会根据结果做出成功或者失败操作。
TokenAuthFilter的作用是:验证是否登录,每次想要访问操作的时候,都得判断有没有登录,首先,先拿到前端送过来handlerd的token字符串,若token为空,没登录;若不为空,验证token是否合法、是否篡改、再和redis中的token字符串比对,如果比对成功,说明是登录用户,然后获取用户的权限,提供一个令牌给用户,此时该用户的状态为登录状态;如果以上条件不满足,就不会提供令牌,因此没有权限,无法访问任何页面。
加载流程
8、方法级别的权限控制
添加方法级别的角色控制,可以通过注解的方式来完成,在需要具有角色或权限方法的上引入 @PreAuthorize 注解。
这里为了方便期间,没有引入数据库,账号和密码都是写死的。
开启权限控制
// 开启方法级别的注解
@EnableGlobalMethodSecurity(prePostEnabled = true)//启动校色认证
控制层的方法上引入注解
@Slf4j
@RestController
@RequestMapping("/first")
public class FirstController {
@GetMapping("/a")
@PreAuthorize("hasAnyAuthority('manager1','manager2')")
public JsonResult a() {
log.info("访问了a");
return ResultTool.success();
}
@GetMapping("/b")
@PreAuthorize("hasAnyAuthority('manager1','manager3')")
public JsonResult b() {
log.info("访问了b");
return ResultTool.success();
}
@GetMapping("/c")
@PreAuthorize("hasAnyAuthority('manager2')")
public JsonResult c() {
log.info("访问了c");
return ResultTool.success();
}
@GetMapping("/d")
@PreAuthorize("hasAnyAuthority('manager4')")
public JsonResult d() {
log.info("访问了d");
return ResultTool.success();
}
}
在TokenAuthFilter中修改
List<String> list = Arrays.asList("manager1", "manager2");
在UserServiceImpl中修改
List<String> auths = Arrays.asList("manager1", "manager2");
效果演示
因为没有写页面,用ApiPost测试效果
没有权限,登录不成功
所以登录将token凭证传入get请求中
登录成功,a可以访问
b也可以成功访问
c也可以成功访问
d不可以访问(没有权限)
只有manager4权限
控制台输出
9、SpringSecurity 基本流程
SpringSecurity 的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器,这里我们可以看看入门案例中的过滤器。
10、SpringSecurity 拦截器
1 . org.springframework.security.web.context.SecurityContextPersistenceFilter
首当其冲的一个过滤器,作用之重要,自不必多言。
SecurityContextPersistenceFilter主要是使用SecurityContextRepository在session中保存或更新一个
SecurityContext,并将SecurityContext给以后的过滤器使用,来为后续filter建立所需的上下文。
SecurityContext中存储了当前用户的认证以及权限信息。
2 . org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter
此过滤器用于集成SecurityContext到Spring异步执行机制中的WebAsyncManager
3 . org.springframework.security.web.header.HeaderWriterFilter
向请求的Header中添加相应的信息,可在http标签内部使用security:headers来控制
4 . org.springframework.security.web.csrf.CsrfFilter
csrf又称跨域请求伪造,SpringSecurity会对所有post请求验证是否包含系统生成的csrf的token信息,
如果不包含,则报错。起到防止csrf攻击的效果。
5. org.springframework.security.web.authentication.logout.LogoutFilter
匹配 URL为/logout的请求,实现用户退出,清除认证信息。
6 . org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
认证操作全靠这个过滤器,默认匹配URL为/login且必须为POST请求。
7 . org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter
如果没有在配置文件中指定认证页面,则由该过滤器生成一个默认认证页面。
8 . org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter
由此过滤器可以生产一个默认的退出登录页面
9 . org.springframework.security.web.authentication.www.BasicAuthenticationFilter
此过滤器会自动解析HTTP请求中头部名字为Authentication,且以Basic开头的头信息。
10 . org.springframework.security.web.savedrequest.RequestCacheAwareFilter
通过HttpSessionRequestCache内部维护了一个RequestCache,用于缓存HttpServletRequest
11 . org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
针对ServletRequest进行了一次包装,使得request具有更加丰富的API
12 . org.springframework.security.web.authentication.AnonymousAuthenticationFilter
当SecurityContextHolder中认证信息为空,则会创建一个匿名用户存入到SecurityContextHolder中。
spring security为了兼容未登录的访问,也走了一套认证流程,只不过是一个匿名的身份。
13 . org.springframework.security.web.session.SessionManagementFilter
SecurityContextRepository限制同一用户开启多个会话的数量
14 . org.springframework.security.web.access.ExceptionTranslationFilter
异常转换过滤器位于整个springSecurityFilterChain的后方,用来转换整个链路中出现的异常
15 . org.springframework.security.web.access.intercept.FilterSecurityInterceptor
获取所配置资源访问的授权信息,根据SecurityContextHolder中存储的用户信息来决定其是否有权限。
11、WebSecurityConfigurerAdapter 接口
/**
* spring security 核心配置文件
*/
@Configuration
public class BrowerSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired //自定义的安全元 数据源 实现FilterInvocationSecurityMetadataSource
private MyInvocationSecurityMetadataSourceService myInvocationSecurityMetadataSourceService;
@Autowired //自定义访问决策器
private MyAccessDecisionManager myAccessDecisionManager;
@Override
protected void configure(HttpSecurity http) throws Exception {
/**
* from表单登录设置
*/
http.formLogin()
.loginPage("") //登录页面 /login
.passwordParameter("") //设置form表单中对应的name参数 默认为 password 下同
.usernameParameter("") //
.defaultSuccessUrl("") //认证成功后的跳转页面 默认跳转页面 可以设置是否总是默认 不是的话可以跳转与用户的target-url
.failureUrl("")
.failureForwardUrl("") //登录失败 转发 的url
.successForwardUrl("") //登录成功 转发 的url 与successHandler对应 即处理完后请求转发的url
.failureHandler(null) //自定义的认证失败 做什么处理
.successHandler(null) //自定义认证成功 后做的处理 ----- 例如 想记录用户信息判断用户状态等
.permitAll() //对于需要所有用户都可以访问的界面 或者url进行设置
.loginProcessingUrl("") //自定义处理认证的url 默认为 /login
.authenticationDetailsSource(null) //自定义身份验证的数据源 理解为查出数据库中的密码 和权限(可以不加) 然后再交给security
修改和替换配置 已经配置好的修改 例如下面修改 安全拦截器的安全数据源
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
public <O extends FilterSecurityInterceptor> O postProcess(
O fsi) {
fsi.setPublishAuthorizationSuccess(true);
//修改成自定义的 安全元数据源 权限的源 !!!!!
fsi.setSecurityMetadataSource(myInvocationSecurityMetadataSourceService);
//修改成自定义的 访问决策器 自定义的
fsi.setAccessDecisionManager(myAccessDecisionManager);
//使用系统的
fsi.setAuthenticationManager(authenticationManager);
return fsi;
}
});
/**
* 请求认证管理
*/
http.authorizeRequests()
.antMatchers("url匹配路径").permitAll() //url匹配路径 permitAll 运行 全部访问 不用认证
.accessDecisionManager(null) //访问决策器
.filterSecurityInterceptorOncePerRequest(true) //过滤每个请求一次的安全拦截器 ???
.anyRequest().authenticated() //其他的请求 需要认证,
.antMatchers("/admin/**").hasRole("ADMIN") //url匹配路径 具有怎样的角色
.antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')") //url匹配路径 具有怎样的角色 或者是权限
;
/**
* anonymous
*
* 匿名访问时 存在默认 用户名 annonymousUser
*/
http.anonymous().disable().csrf().disable(); //禁止匿名 关闭csrf
/**
* 登出操作管理
*/
http.logout() //登出处理
.logoutUrl("/my/logout")
.logoutSuccessUrl("/my/index")
.logoutSuccessHandler(null)
.invalidateHttpSession(true)
.addLogoutHandler(null)
.deleteCookies("cookieNamesToClear")
;
/**
* session 会话管理
*/
http.sessionManagement() //session管理
.maximumSessions(2) //最大session 数量 --用户
.maxSessionsPreventsLogin(false) //超过最大sessin数量后时候阻止登录
.expiredUrl("/") //会话失效后跳转的url
.expiredSessionStrategy(null) //自定义session 过期错略
.sessionRegistry(null) //自定义的session 注册 表
;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
/**
* 基础的配置
*/
auth
/**
* 认证 时触发的事件
*/
.authenticationEventPublisher(null)
/**
* 用户细节服务
*
* 认证管理器数据的来源 吧 用户身份凭证信息和 权限信息
*/
.userDetailsService(null)
/**
* 密码编辑器 对密码进行加密
*/
.passwordEncoder(null)
;
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
public void configure(WebSecurity web) throws Exception {
/**
* 不进行拦截的mvc
*/
web.ignoring().mvcMatchers();
/**
* 添加自定义的 安全过滤器
*/
web.addSecurityFilterChainBuilder(null);
}
}
12、综合案例
前期准备
准备数据库
前提:(6张表)
用户、角色(一组权限的统称)、权限(能不能干某件事)三者之间存在多对多的关系
多个角色有多个权限、一个权限隶属于多个角色;
用户可以有多个角色,一个角色可以被多个用户所拥有;
用户可以有多个权限,一个权限可以被多个用户所拥有.
# 权限管理
show databases ;
# 创建数据库
create database powers;
use powers;
# 创建表
create table user
(
id int primary key auto_increment COMMENT '用户',
username varchar(20) not null unique COMMENT '姓名',
password varchar(100) not null COMMENT '密码',
nickname varchar(20) default '用户' COMMENT '昵称',
state int default 1 COMMENT '状态'
)COMMENT='用户表';
# 权限表
create table power
(
id int primary key auto_increment COMMENT '权限ID',
powername varchar(20) not null unique COMMENT '权限名称',
powerurl varchar(100) COMMENT '权限URL'
)COMMENT='权限表';
# 角色表
create table role
(
id int(11) primary key auto_increment COMMENT '角色ID',
rolename varchar(20) not null unique COMMENT '角色名称'
) COMMENT='角色表';
# 用户-权限表
create table userpower
(
id int primary key auto_increment COMMENT '用户权限ID',
uid int COMMENT '用户ID',
pid int COMMENT '权限ID',
foreign key(uid) references user(id),
foreign key(pid) references power(id)
) COMMENT='用户-权限表';
# 角色-权限表
create table rolepower
(
id int primary key auto_increment COMMENT '角色权限ID',
rid int COMMENT '角色ID',
pid int COMMENT '权限ID',
foreign key(rid) references role(id),
foreign key(pid) references power(id)
) COMMENT='角色-权限表';
# 用户-角色表
create table userrole
(
id int primary key auto_increment COMMENT '用户角色ID',
rid int COMMENT '角色ID',
uid int COMMENT '用户ID',
foreign key(rid) references role(id),
foreign key(uid) references user(id)
) COMMENT='用户-角色表';
# 给用户表录入测试数据
insert into user values(null,'admin','123456','管理员',1);
insert into user values(null,'zhangsan','123456','张三',1);
insert into user values(null,'lisi','123456','李四',1);
insert into user values(null,'wangwu','123456','王五',1);
# 给权限表录入测试数据
insert into power values(null,'图书查询','book/find');
insert into power values(null,'图书修改','book/update');
insert into power values(null,'图书添加','book/save');
insert into power values(null,'图书删除','book/delete');
insert into power values(null,'用户查询','user/find');
insert into power values(null,'用户修改','user/update');
insert into power values(null,'用户添加','user/save');
insert into power values(null,'用户删除','user/delete');
insert into power values(null,'订单查询','order/find');
insert into power values(null,'订单修改','order/update');
insert into power values(null,'订单添加','order/save');
insert into power values(null,'订单删除','order/delete');
# 给角色表录入测试数据
insert into role values(null,'图书管理');
insert into role values(null,'用户管理');
insert into role values(null,'订单管理');
insert into role values(null,'管理员');
# 给角色-权限表录入测试数据
insert into rolepower values(null,4,1);
insert into rolepower values(null,4,2);
insert into rolepower values(null,4,3);
insert into rolepower values(null,4,4);
insert into rolepower values(null,4,5);
insert into rolepower values(null,4,6);
insert into rolepower values(null,4,7);
insert into rolepower values(null,4,8);
insert into rolepower values(null,4,9);
insert into rolepower values(null,4,10);
insert into rolepower values(null,4,11);
insert into rolepower values(null,4,12);
insert into rolepower values(null,1,9);
insert into rolepower values(null,1,10);
insert into rolepower values(null,1,11);
insert into rolepower values(null,1,12);
insert into rolepower values(null,2,1);
insert into rolepower values(null,2,2);
insert into rolepower values(null,2,3);
insert into rolepower values(null,2,4);
insert into rolepower values(null,3,5);
insert into rolepower values(null,3,6);
insert into rolepower values(null,3,7);
insert into rolepower values(null,3,8);
# 给用户-角色表录入测试数据
insert into userrole values(null,4,1);
insert into userrole values(null,1,2);
insert into userrole values(null,2,3);
insert into userrole values(null,3,3);
# 给用户-权限表录入测试数据
insert into userpower values(null,2,2);
insert into userpower values(null,2,3);
insert into userpower values(null,3,2);
insert into userpower values(null,2,10);
select * from userpower;
select * from user;
select * from role;
select * from power;
注:在SQL语句中,COMMENT关键字用于为某个字段或表添加注释,目的是为了更好地理解和维护数据库中的数据,使其更易于使用和管理。
后端部分
搭建项目
导入依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.9</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.32</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring_boot_commons</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
application.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/powers # 数据库连接地址
username: root # 登录数据库的用户名
password: root # 登录数据库的密码
driver-class-name: com.mysql.cj.jdbc.Driver # 数据库驱动的类名
type: com.alibaba.druid.pool.DruidDataSource # 数据库连接池的类型
redis:
host: 192.168.65.3 # Redis的主机地址
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 日志实现方式
map-underscore-to-camel-case: false # 是否将下划线转换为驼峰命名
查询权限
bean包
Power
@Data
public class Power implements Serializable {
@TableId(type = IdType.AUTO) // 表示该字段是数据库中的主键,类型为自增长
private Integer id ; // 主键
private String powerName; // 权力名称
private String powerUrl; // 权力地址
}
Role
@Data
public class Role implements Serializable {
@TableId(type = IdType.AUTO) // 表示该字段是数据库中的主键,类型为自增长
private Integer id; // 主键
private String roleName; // 角色名称
@TableField(exist = false) // 表示该字段在数据库中不存在,为true时,表示该字段为null,为false时,表示该字段存在但不显示在控制台输出中
private Set<Power> powers; // 权力集合
}
SecurityUser
@Data
public class SecurityUser implements UserDetails {
private User user; // 用户信息实体
private List<String> powers; // 权力列表
@Override // 实现 UserDetails 接口的 getAuthorities() 方法
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> list = new ArrayList<>(); // 创建一个 Collection 对象
powers.forEach((e) -> list.add(new SimpleGrantedAuthority(e))); // 遍历权力列表,将每个权力转化为一个 GrantedAuthority 对象,并添加到 Collection 对象中
return list; // 返回 Collection 对象
}
@Override // 实现 UserDetails 接口的 getPassword() 方法
public String getPassword() {
return user.getPassword(); // 返回用户的密码
}
@Override // 实现 UserDetails 接口的 getUsername() 方法
public String getUsername() {
return user.getUsername(); // 返回用户的用户名
}
@Override // 实现 UserDetails 接口的 isAccountNonExpired() 方法
public boolean isAccountNonExpired() {
return true; // 返回 true,表示用户的账号未过期
}
@Override // 实现 UserDetails 接口的 isAccountNonLocked() 方法
public boolean isAccountNonLocked() {
return true; // 返回 true,表示用户的账号未被锁定
}
@Override // 实现 UserDetails 接口的 isCredentialsNonExpired() 方法
public boolean isCredentialsNonExpired() {
return true; // 返回 true,表示用户的凭证未过期
}
@Override // 实现 UserDetails 接口的 isEnabled() 方法
public boolean isEnabled() {
return true; // 返回 true,表示用户已启用
}
}
User
@Data // 自动为该类生成getter、setter、equals、hashcode等方法
public class User implements Serializable {
@TableId(type = IdType.AUTO) // 表示该字段是数据库中的主键,类型为自增长
private Integer id; // 用户ID
private String username; // 用户名
private String password; // 密码
private String nickname; // 昵称
private Integer state; // 状态,0表示禁用,1表示启用
@TableField(exist = false) // 表示该字段在数据库中不存在,为true时,表示该字段为null,为false时,表示该字段存在但不显示在控制台输出中
private Set<Power> powers; // 权力集合
@TableField(exist = false) // 表示该字段在数据库中不存在,为true时,表示该字段为null,为false时,表示该字段存在但不显示在控制台输出中
private Set<Role> roles; // 角色集合
}
mapper包
UserMapper
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
PowerMapper
@Mapper
public interface PowerMapper extends BaseMapper<Power> {
@Select("select * from power where id in" +
"(select pid from userpower where uid=#{id}) " +
"or id in(select pid from rolepower where rid in " +
"(select rid from userrole where uid=#{id}))")
List<Power> findPowerByUserId(int id);
}
service包
UserService
public interface UserService extends IService<User> {
}
impl——>UserServiceImpl
@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService, UserDetailsService {
@Resource
private UserMapper userMapper;
@Resource
private PowerMapper powerMapper;
@Resource
private PasswordEncoder bCryptPasswordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("username", username);
User user = userMapper.selectOne(wrapper);
if (user == null) {
throw new UsernameNotFoundException("账号不存在!");
}
user.setPassword(bCryptPasswordEncoder.encode(user.getPassword()));
SecurityUser securityUser = new SecurityUser();
securityUser.setUser(user);
// 获取当前用户的所有权限
List<Power> powers = powerMapper.findPowerByUserId(user.getId());
List<String> powerList = new ArrayList<>();
powers.forEach((e) -> powerList.add(e.getPowerName()));
securityUser.setPowers(powerList);
log.info("用户的所有权限信息:{}", powerList);
return securityUser;
}
}
filter包
LoginTokenFilter
@Slf4j
public class LoginTokenFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
private StringRedisTemplate stringRedisTemplate;
private PasswordEncoder passwordEncoder;
public LoginTokenFilter(AuthenticationManager authenticationManager, StringRedisTemplate stringRedisTemplate, PasswordEncoder passwordEncoder) {
this.authenticationManager = authenticationManager;
this.stringRedisTemplate = stringRedisTemplate;
this.passwordEncoder = passwordEncoder;
// 关闭登录只允许 post
this.setPostOnly(false);
// 设置登陆路径,并且post请求
this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/login", "POST"));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
log.info("开始用户登陆操作");
String username = request.getParameter("username");
String password = request.getParameter("password");
String encoderPassword = passwordEncoder.encode(password);
log.info("账号:{},密码:{},加密后密码:{}", username, password, encoderPassword);
return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
log.info("登陆成功");
// 1. 生成jwt字符串
SecurityUser user = (SecurityUser) authResult.getPrincipal(); // 获取到登陆成功后保存的user
User commonsUser = new User();
commonsUser.setId(user.getUser().getId());
commonsUser.setNickname(user.getUser().getNickname());
commonsUser.setUsername(user.getUser().getUsername());
String token = JwtConfig.getJwtToken(commonsUser);
// 2. 保存到redis
stringRedisTemplate.opsForValue().set("TOKEN:" + user.getUser().getId(), token, 1, TimeUnit.DAYS);
// 3. 返回给前端token
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.println(JSONArray.toJSONString(ResultTool.success(token)));
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
log.info("账号或者密码错误");
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.println(JSONArray.toJSONString(ResultTool.fail("账号或者密码错误")));
}
}
config包
SecurityConfig
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private UserDetailsService userDetailsService;
@Resource
private PasswordEncoder passwordEncoder;
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling()
.and().csrf().disable() // 关闭 csrf 跨域请求
.cors().and() // security允许跨域
.formLogin()
.loginProcessingUrl("/login") // 设定登录请求接口
.usernameParameter("username")
.passwordParameter("password")
.permitAll()
.and()
.authorizeRequests() // 请求设置
.antMatchers("/login").permitAll() // 配置不需要认证的接口
.anyRequest().authenticated() // 任何请求都需要认证
.and()
.addFilter(new LoginTokenFilter(authenticationManager(), stringRedisTemplate, passwordEncoder)) // 配置登陆filter
.httpBasic();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
}
}
启动类
@SpringBootApplication
public class SpringBootSecurity3Application {
@Bean
PasswordEncoder getPasswordEncoder() {
return new BCryptPasswordEncoder();
}
public static void main(String[] args) {
SpringApplication.run(SpringBootSecurity3Application.class, args);
}
}
测试结果
1、输入错误
2、输入正确
判断权限
controller包
BookController
@Slf4j
@RestController
@RequestMapping("/book")
public class BookController {
@GetMapping("/find")
@PreAuthorize("hasAnyAuthority('图书查询')")
public JsonResult find() {
log.info("执行了查询操作");
return ResultTool.success("图书查询");
}
@PutMapping("/update")
@PreAuthorize("hasAnyAuthority('图书修改')")
public JsonResult update() {
log.info("执行了修改操作");
return ResultTool.success("图书修改");
}
@PostMapping("/save")
@PreAuthorize("hasAnyAuthority('图书添加')")
public JsonResult save() {
log.info("执行了添加操作");
return ResultTool.success("图书添加");
}
@DeleteMapping("/delete")
@PreAuthorize("hasAnyAuthority('图书删除')")
public JsonResult delete() {
log.info("执行了删除操作");
return ResultTool.success("图书删除");
}
}
OrderController
@Slf4j
@RestController
@RequestMapping("/order")
public class OrderController {
@GetMapping("/find")
@PreAuthorize("hasAnyAuthority('订单查询')")
public JsonResult find() {
log.info("执行了查询操作");
return ResultTool.success("订单查询");
}
@PutMapping("/update")
@PreAuthorize("hasAnyAuthority('订单修改')")
public JsonResult update() {
log.info("执行了修改操作");
return ResultTool.success("订单修改");
}
@PostMapping("/save")
@PreAuthorize("hasAnyAuthority('订单添加')")
public JsonResult save() {
log.info("执行了添加操作");
return ResultTool.success("订单添加");
}
@DeleteMapping("/delete")
@PreAuthorize("hasAnyAuthority('订单删除')")
public JsonResult delete() {
log.info("执行了删除操作");
return ResultTool.success("订单删除");
}
}
UserController
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping("/find")
@PreAuthorize("hasAnyAuthority('用户查询')")
public JsonResult find() {
log.info("执行了查询操作");
return ResultTool.success("用户查询");
}
@PutMapping("/update")
@PreAuthorize("hasAnyAuthority('用户修改')")
public JsonResult update() {
log.info("执行了修改操作");
return ResultTool.success("用户修改");
}
@PostMapping("/save")
@PreAuthorize("hasAnyAuthority('用户添加')")
public JsonResult save() {
log.info("执行了添加操作");
return ResultTool.success("用户添加");
}
@DeleteMapping("/delete")
@PreAuthorize("hasAnyAuthority('用户删除')")
public JsonResult delete() {
log.info("执行了删除操作");
return ResultTool.success("用户删除");
}
}
filter包
IsLoginFilter
@Slf4j
public class IsLoginFilter extends BasicAuthenticationFilter {
private StringRedisTemplate stringRedisTemplate;
public IsLoginFilter(AuthenticationManager authenticationManager, StringRedisTemplate stringRedisTemplate) {
super(authenticationManager);
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("开始校验用户是否登陆");
// 1. 获取到前端发送过来的token
String token = request.getHeader("token");
log.info("前端发送过来的token:{}", token);
// 2. 校验token
// 2.1 是否发送
if (token != null) {
// 2.2 是否合法
boolean flag = JwtConfig.checkToken(token);
log.info("是否合法:{}", flag);
if (flag) {
Claims claims = JwtConfig.parseJWT(token);
String id = claims.get("id").toString();
String redisToken = stringRedisTemplate.opsForValue().get("TOKEN:" + id);
log.info("redis的token:{}", redisToken);
// 2.3 是否篡改
if (token.equals(redisToken)) {
// 3. 获取权限 从redis中获取权限
String s = stringRedisTemplate.opsForValue().get("POWER:" + id);
List<String> list = JSONArray.parseArray(s, String.class);
List<GrantedAuthority> auths = new ArrayList<>();
list.forEach((e) -> {
GrantedAuthority authority = new SimpleGrantedAuthority(e);
auths.add(authority);
});
log.info("成功,下方权限!");
// 4. 下方权限
UsernamePasswordAuthenticationToken upat = new UsernamePasswordAuthenticationToken(id, token, auths);
// 有权限,则放入权限上下文中
SecurityContextHolder.getContext().setAuthentication(upat);
}
}
} else {
// 清空操作
SecurityContextHolder.getContext().setAuthentication(null);
}
chain.doFilter(request, response);
}
}
LoginTokenFilter
@Slf4j
public class LoginTokenFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
private StringRedisTemplate stringRedisTemplate;
private PasswordEncoder passwordEncoder;
public LoginTokenFilter(AuthenticationManager authenticationManager, StringRedisTemplate stringRedisTemplate, PasswordEncoder passwordEncoder) {
this.authenticationManager = authenticationManager;
this.stringRedisTemplate = stringRedisTemplate;
this.passwordEncoder = passwordEncoder;
// 关闭登录只允许 post
this.setPostOnly(false);
// 设置登陆路径,并且post请求
this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/login", "POST"));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
log.info("开始用户登陆操作");
String username = request.getParameter("username");
String password = request.getParameter("password");
String encoderPassword = passwordEncoder.encode(password);
log.info("账号:{},密码:{},加密后密码:{}", username, password, encoderPassword);
return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
log.info("登陆成功");
// 1. 生成jwt字符串
SecurityUser user = (SecurityUser) authResult.getPrincipal(); // 获取到登陆成功后保存的user
User commonsUser = new User();
commonsUser.setId(user.getUser().getId());
commonsUser.setNickname(user.getUser().getNickname());
commonsUser.setUsername(user.getUser().getUsername());
String token = JwtConfig.getJwtToken(commonsUser);
// 2. 保存到redis
//存储令牌
stringRedisTemplate.opsForValue().set("TOKEN:" + user.getUser().getId(), token, 1, TimeUnit.DAYS);
//存储权限
stringRedisTemplate.opsForValue().set("POWER:"+user.getUser().getId(),JSONArray.toJSONString(user.getPowers()),1, TimeUnit.DAYS);
// 3. 返回给前端token
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.println(JSONArray.toJSONString(ResultTool.success(token)));
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
log.info("账号或者密码错误");
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.println(JSONArray.toJSONString(ResultTool.fail("账号或者密码错误")));
}
}
1.登陆成功后把权限信息存到redis中
//存储权限 stringRedisTemplate.opsForValue().set("POWER:"+user.getUser().getId(),JSONArray.toJSONString(user.getPowers()),1, TimeUnit.DAYS);
2.从redis中获取权限
// 3. 获取权限 从redis中获取权限
String s = stringRedisTemplate.opsForValue().get("POWER:" + id);
List<String> list = JSONArray.parseArray(s, String.class);
config
SecurityConfig
.addFilter(new IsLoginFilter(authenticationManager(),stringRedisTemplate))
handler包
NotAccessHandler
@Component
@Slf4j
public class NotAccessHandler implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
log.info("用户没有登录");
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.println(JSONArray.toJSONString(ResultTool.fail("资源无法访问!")));
}
}
再给SecurityConfig加如下内容
@Resource
private NotAccessHandler notAccessHandler;
.authenticationEntryPoint(notAccessHandler)// 未登录 handler
测试结果
1、先登录
2、携带token访问成功
3、控制台信息
4、测试没有用户查询的权限
操作权限
SecurityConfig
放行前端部分
.antMatchers("/login", "/login.html", "/", "/index.html", "/assort.html", "/js/*", "/img/*", "/css/*").permitAll() // 配置不需要认证的接口
前端部分
导入js包
assort.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<!-- 引入elementUI样式 -->
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
<style>
.el-header {
background-color: #B3C0D1;
color: #333;
line-height: 60px;
}
.el-aside {
color: #333;
}
</style>
</head>
<body>
<div id="app">
<el-container style="height: 800px; border: 1px solid #eee">
<el-aside width="200px" style="background-color: rgb(238, 241, 246)">
<el-menu :default-openeds="['1', '3']">
<el-submenu index="1">
<template slot="title"><i class="el-icon-message"></i>用户管理</template>
<el-menu-item-group>
<el-menu-item index="1-1">用户查询</el-menu-item>
<el-menu-item index="1-2">用户添加</el-menu-item>
</el-menu-item-group>
</el-submenu>
<el-submenu index="2">
<template slot="title"><i class="el-icon-menu"></i>分类管理</template>
<el-menu-item-group>
<el-menu-item index="2-1">分类查询</el-menu-item>
<el-menu-item index="2-2">分类添加</el-menu-item>
</el-menu-item-group>
</el-submenu>
<el-submenu index="3">
<template slot="title"><i class="el-icon-setting"></i>图书管理</template>
<el-menu-item-group>
<el-menu-item index="3-1">图书查询</el-menu-item>
<el-menu-item index="3-2">图书添加</el-menu-item>
</el-menu-item-group>
</el-submenu>
</el-menu>
</el-aside>
<el-container>
<el-header style="text-align: right; font-size: 12px">
<el-dropdown>
<i class="el-icon-setting" style="margin-right: 15px"></i>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item>查看</el-dropdown-item>
<el-dropdown-item>新增</el-dropdown-item>
<el-dropdown-item>删除</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<span>王小虎</span>
</el-header>
<el-main>
{{message}}
<el-table :data="tableData">
<el-table-column prop="id" label="编号" width="60">
</el-table-column>
<el-table-column prop="name" label="分类名" width="120">
</el-table-column>
<el-table-column prop="desc" label="备注" width="190">
</el-table-column>
<el-table-column prop="state" label="状态" width="60">
</el-table-column>
<el-table-column label="操作">
<template slot-scope="scope">
<el-button
size="mini" @click="showUpdateDialog(scope.row)">编辑
</el-button>
<el-button
size="mini"
type="danger" @click="deleteAssort(scope.row)">删除
</el-button>
</template>
</el-table-column>
</el-table>
</el-main>
</el-container>
</el-container>
<!--修改的模态框-->
<el-dialog title="编辑分类" :visible.sync="showUpdateAssortDialog">
{{errorMsg}}
<el-form :model="updateForm">
<el-form-item label="分类名称" :label-width="formLabelWidth">
<el-input v-model="updateForm.name" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="备注信息" :label-width="formLabelWidth">
<el-input v-model="updateForm.desc" autocomplete="off"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="showUpdateAssortDialog=false">取 消</el-button>
<el-button type="primary" @click="updateAssort">确 定</el-button>
</div>
</el-dialog>
</div>
</body>
</html>
<!--引入vue-->
<script src="js/vue.js"></script>
<!--引入axios-->
<script src="js/axios.min.js"></script>
<!-- 引入elementUI组件库 -->
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
<script>
new Vue({
el: '#app',
data() {
return {
//这里定义变量
tableData: [
{
id: 1,
name: 'aa',
desc: 'bb',
state: 1
}
],
showUpdateAssortDialog: false,
formLabelWidth: '120px',
updateForm: {
id: '',
name: '',
desc: ''
},
isExist: false,
errorMsg: '',
message: ''
}
},
methods: {
loadAssortData() {
let _this = this
// 从后端获取到数据,把数据给tableData赋值
axios.get('/user/find',{
headers:{
token:window.localStorage.getItem('token')
}
}).then((response) => {
// 获取到后端返回的数据
let d = response.data
if (d.success) {
_this.message = d.data
} else {
alert(response.data.error);
_this.message = d.error
}
//_this.tableData = d.data
})
},
deleteAssort(row) {
let _this = this
this.$confirm('此操作将永久删除该分类, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 点击确定了 调用后端的删除操作并传递id,成功后刷新页面
// axios.get('assort/delete?id='+row.id)
axios.get('/user/delete', {
params: {
id: row.id
},
headers:{
token:window.localStorage.getItem('token')
}
}).then((response) => {
if (response.data.success) {
this.$message({
type: 'success',
message: '已成功删除'
});
_this.message = response.data.data
} else {
alert(response.data.error);
_this.message = response.data.error
}
//_this.tableData = response.data.data
//console.log(_this.tableData)
//_this.loadAssortData()
})
}).catch(() => {
this.$message({
type: 'info',
message: '已取消删除'
});
});
},
showUpdateDialog(row) {
this.updateForm.name = row.name
this.updateForm.desc = row.desc
this.updateForm.id = row.id
this.showUpdateAssortDialog = true
},
updateAssort() {
this.isExist = true
if (this.isExist) {
let _this = this
axios.get('/user/update',{
headers:{
token:window.localStorage.getItem('token')
}
}).then((response) => {
if (response.data.success) {
this.$message({
type: 'success',
message: '已成功修改'
});
_this.message = response.data.data
} else {
alert(response.data.error);
_this.message = response.data.error
}
//_this.loadAssortData()
_this.showUpdateAssortDialog = false
})
}
},
checkNameIsExist() {
// 获取到名称
let name = this.updateForm.name
let _this = this
// 发送给后端
axios.get('assort/exist', {
params: {
name: name
}
}).then((response) => {
let success = response.data.success
if (success) {
_this.isExist = true
_this.errorMsg = ''
} else {
_this.isExist = false
_this.errorMsg = response.data.error
}
})
}
},
created() {
// 页面加载后执行
this.loadAssortData()
}
})
</script>
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<!-- 引入elementUI样式 -->
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
<style>
.el-header {
background-color: #B3C0D1;
color: #333;
line-height: 60px;
}
.el-aside {
color: #333;
}
</style>
</head>
<body>
<div id="app">
<el-container style="height: 800px; border: 1px solid #eee">
<el-aside width="200px" style="background-color: rgb(238, 241, 246)">
<el-menu :default-openeds="['1', '3']">
<el-submenu index="1">
<template slot="title"><i class="el-icon-message"></i>用户管理</template>
<el-menu-item-group>
<el-menu-item index="1-1">用户查询</el-menu-item>
<el-menu-item index="1-2">用户添加</el-menu-item>
</el-menu-item-group>
</el-submenu>
<el-submenu index="2">
<template slot="title"><i class="el-icon-menu"></i>分类管理</template>
<el-menu-item-group>
<el-menu-item index="2-1">分类查询</el-menu-item>
<el-menu-item index="2-2">分类添加</el-menu-item>
</el-menu-item-group>
</el-submenu>
<el-submenu index="3">
<template slot="title"><i class="el-icon-setting"></i>图书管理</template>
<el-menu-item-group>
<el-menu-item index="3-1">图书查询</el-menu-item>
<el-menu-item index="3-2">图书添加</el-menu-item>
</el-menu-item-group>
</el-submenu>
</el-menu>
</el-aside>
<el-container>
<el-header style="text-align: right; font-size: 12px">
<el-dropdown>
<i class="el-icon-setting" style="margin-right: 15px"></i>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item>查看</el-dropdown-item>
<el-dropdown-item>新增</el-dropdown-item>
<el-dropdown-item>删除</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<span>王小虎</span>
</el-header>
<el-main>
<img src="img/background.webp"/>
</el-main>
</el-container>
</el-container>
</div>
</body>
</html>
<!--引入vue-->
<script src="js/vue.js"></script>
<!--引入axios-->
<script src="js/axios.min.js"></script>
<!-- 引入elementUI组件库 -->
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
<script>
new Vue({
el: '#app'
})
</script>
login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
{{error}}<br/>
账号:<input type="text" v-model="username"/><br/>
密码:<input type="password" v-model="password"/><br/>
<button @click="login">登陆</button>
</div>
</body>
</html>
<!--引入vue-->
<script src="js/vue.js"></script>
<!--引入axios-->
<script src="js/axios.min.js"></script>
<script>
new Vue({
el: '#app',
data() {
return {
username: '',
password: '',
error: ''
}
},
methods: {
login() {
let data = new URLSearchParams()
data.append('username', this.username)
data.append('password', this.password)
let _this = this
axios({
method: 'post',
url: '/login',
data: data
}).then((response) => {
if (response.data.success) {
window.localStorage.setItem('token', response.data.data)
location.href = 'assort.html';
} else {
_this.error = response.data.error
}
})
}
}
})
</script>
heandler包
Fail403Handler
@Component
public class Fail403Handler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.println(JSONArray.toJSONString(ResultTool.fail( "资源无法访问!")));
}
}
SecurityConfig
@Resource
private Fail403Handler fail403Handler;
.accessDeniedHandler(fail403Handler)
测试结果
出现问题
将UserController里面修改为
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping("/find")
@PreAuthorize("hasAnyAuthority('用户查询')")
public JsonResult find() {
log.info("执行了查询操作");
return ResultTool.success("用户查询");
}
@GetMapping("/update")
@PreAuthorize("hasAnyAuthority('用户修改')")
public JsonResult update() {
log.info("执行了修改操作");
return ResultTool.success("用户修改");
}
@GetMapping("/save")
@PreAuthorize("hasAnyAuthority('用户添加')")
public JsonResult save() {
log.info("执行了添加操作");
return ResultTool.success("用户添加");
}
@GetMapping("/delete")
@PreAuthorize("hasAnyAuthority('用户删除')")
public JsonResult delete() {
log.info("执行了删除操作");
return ResultTool.success("用户删除");
}
}
再次测试
点击编辑
点击删除
控制台
修改权限1
为数据添加数据
insert into power values (null,'权限查询','power/find');
insert into power values (null,'权限修改','power/update');
insert into rolepower values(null,4,13);
insert into rolepower values(null,4,14);
UserController
@Resource
private UserService userService;
@GetMapping("/find")
@PreAuthorize("hasAnyAuthority('用户查询')")
public JsonResult find() {
log.info("执行了查询操作");
return ResultTool.success(userService.list());
}
给公共模块的ResuTool添加,NotAccessHandler和Fail403Handler修改
public static JsonResult fail(int code,String msg) {
return new JsonResult(false, msg, code, null);
}
out.println(JSONArray.toJSONString(ResultTool.fail(401,"用户没有登陆!")));
out.println(JSONArray.toJSONString(ResultTool.fail(403, "资源无法访问!")));
前端部分
测试1
页面效果:
控制台输出:
再加一个按钮,进行权限操作,跳转到另一个页面,可以编辑其他用户的权限问题(当点击按钮的时候,会把该用户的权限显示出来,但不能修改自己权限,加个if判断就可以)
<el-button
size="mini"
type="danger" @click="showPower(scope.row.id)">权限操作
</el-button>
showPower(uid) {
window.sessionStorage.setItem('UID', uid)
location.href = '/power.html'
},
再写一个power.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<!-- 引入elementUI样式 -->
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
<style>
.el-header {
background-color: #B3C0D1;
color: #333;
line-height: 60px;
}
.el-aside {
color: #333;
}
</style>
</head>
<body>
<div id="app">
<el-container style="height: 800px; border: 1px solid #eee">
<el-aside width="200px" style="background-color: rgb(238, 241, 246)">
<el-menu :default-openeds="['1', '3']">
<el-submenu index="1">
<template slot="title"><i class="el-icon-message"></i>用户管理</template>
<el-menu-item-group>
<el-menu-item index="1-1">用户查询</el-menu-item>
<el-menu-item index="1-2">用户添加</el-menu-item>
</el-menu-item-group>
</el-submenu>
<el-submenu index="2">
<template slot="title"><i class="el-icon-menu"></i>分类管理</template>
<el-menu-item-group>
<el-menu-item index="2-1">分类查询</el-menu-item>
<el-menu-item index="2-2">分类添加</el-menu-item>
</el-menu-item-group>
</el-submenu>
<el-submenu index="3">
<template slot="title"><i class="el-icon-setting"></i>图书管理</template>
<el-menu-item-group>
<el-menu-item index="3-1">图书查询</el-menu-item>
<el-menu-item index="3-2">图书添加</el-menu-item>
</el-menu-item-group>
</el-submenu>
</el-menu>
</el-aside>
<el-container>
<el-header style="text-align: right; font-size: 12px">
<el-dropdown>
<i class="el-icon-setting" style="margin-right: 15px"></i>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item>查看</el-dropdown-item>
<el-dropdown-item>新增</el-dropdown-item>
<el-dropdown-item>删除</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<span>王小虎</span>
</el-header>
<el-main>
{{powers}}
</el-main>
</el-container>
</el-container>
</div>
</body>
</html>
<!--引入vue-->
<script src="js/vue.js"></script>
<!--引入axios-->
<script src="js/axios.min.js"></script>
<!-- 引入elementUI组件库 -->
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
<script>
new Vue({
el: '#app',
data() {
return {
uid: 0,
powers: []
}
},
methods: {
loadUserPowerById() {
console.log(window.localStorage.getItem('token'))
let _this = this
axios.get('/power/find', {
params: {
id: _this.uid
},
headers: {
token: window.localStorage.getItem('token')
}
}).then((response) => {
if (response.data.success) {
_this.powers = response.data.data
} else {
console.log(response.data.error)
}
})
}
},
created() {
this.uid = window.sessionStorage.getItem('UID')
if (this.uid === undefined || this.uid === 0) {
location.href = 'assort.html'
}
this.loadUserPowerById()
}
})
</script>
PowerService
public interface PowerService extends IService<Power> {
JsonResult findPowerByUserId(int id);
}
PowerServiceImpl
@Service
public class PowerServiceImpl extends ServiceImpl<PowerMapper, Power> implements PowerService {
@Resource
private PowerMapper powerMapper;
@Override
public JsonResult findPowerByUserId(int id) {
return ResultTool.success(powerMapper.findPowerByUserId(id));
}
}
PowerController
@Slf4j
@RestController
@RequestMapping("/power")
public class PowerController {
@Resource
private PowerService service;
@GetMapping("/find")
@PreAuthorize("hasAnyAuthority('权限查询')")
public JsonResult find(int id) {
return service.findPowerByUserId(id);
}
}
SecurityConfig
.antMatchers("/login", "/login.html", "/", "/index.html", "/assort.html","/power.html", "/js/*", "/img/*", "/css/*").permitAll() // 配置不需要认证的接口
测试2
修改权限2
修改power.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<!-- 引入elementUI样式 -->
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
<style>
.el-header {
background-color: #B3C0D1;
color: #333;
line-height: 60px;
}
.el-aside {
color: #333;
}
</style>
</head>
<body>
<div id="app">
<el-container style="height: 800px; border: 1px solid #eee">
<el-aside width="200px" style="background-color: rgb(238, 241, 246)">
<el-menu :default-openeds="['1', '3']">
<el-submenu index="1">
<template slot="title"><i class="el-icon-message"></i>用户管理</template>
<el-menu-item-group>
<el-menu-item index="1-1">用户查询</el-menu-item>
<el-menu-item index="1-2">用户添加</el-menu-item>
</el-menu-item-group>
</el-submenu>
<el-submenu index="2">
<template slot="title"><i class="el-icon-menu"></i>分类管理</template>
<el-menu-item-group>
<el-menu-item index="2-1">分类查询</el-menu-item>
<el-menu-item index="2-2">分类添加</el-menu-item>
</el-menu-item-group>
</el-submenu>
<el-submenu index="3">
<template slot="title"><i class="el-icon-setting"></i>图书管理</template>
<el-menu-item-group>
<el-menu-item index="3-1">图书查询</el-menu-item>
<el-menu-item index="3-2">图书添加</el-menu-item>
</el-menu-item-group>
</el-submenu>
</el-menu>
</el-aside>
<el-container>
<el-header style="text-align: right; font-size: 12px">
<el-dropdown>
<i class="el-icon-setting" style="margin-right: 15px"></i>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item>查看</el-dropdown-item>
<el-dropdown-item>新增</el-dropdown-item>
<el-dropdown-item>删除</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<span>王小虎</span>
</el-header>
<el-main>
<el-breadcrumb separator="/">
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
<el-breadcrumb-item><a href="/assort.html">用户管理</a></el-breadcrumb-item>
<el-breadcrumb-item><a href="/power.html">权限管理</a></el-breadcrumb-item>
</el-breadcrumb>
<h3>你的权限</h3>
<span v-for="p in allPowers">
<input type="checkbox" :value="p.id" v-model="checkedPower"/>
<label>{{p.powerName}}</label>
</span>
<br/>
<button @click="changePower">修改权限</button>
</el-main>
</el-container>
</el-container>
</div>
</body>
</html>
<!--引入vue-->
<script src="js/vue.js"></script>
<!--引入axios-->
<script src="js/axios.min.js"></script>
<!-- 引入elementUI组件库 -->
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
<script>
new Vue({
el: '#app',
data() {
return {
uid: 0,
allPowers: [], // 所有的权限
powers: [], // 我的权限
checkedPower: [] //被选中的权限 id
}
},
methods: {
loadAllPowers() {
let _this = this
axios.get('/power/findall', {
headers: {
token: window.localStorage.getItem('token')
}
}).then((response) => {
if (response.data.success) {
_this.allPowers = response.data.data
} else {
console.log(response.data.error)
}
})
},
loadUserPowerById() {
console.log(window.localStorage.getItem('token'))
let _this = this
axios.get('/power/find', {
params: {
id: _this.uid
},
headers: {
token: window.localStorage.getItem('token')
}
}).then((response) => {
if (response.data.success) {
_this.powers = response.data.data
for (let i = 0; i < _this.powers.length; i++) {
_this.checkedPower[i] = _this.powers[i].id;
}
} else {
console.log(response.data.error)
}
})
console.log(_this.powers);
console.log(_this.checkedPower);
},
changePower() {
let _this = this
let data = new URLSearchParams()
data.append("uid", _this.uid)
data.append("checkedPower", _this.checkedPower)
axios({
method: 'put',
url: 'power/update',
data: data,
headers: {
token: window.localStorage.getItem('token')
}
}).then((response) => {
location.href = 'power.html'
})
}
},
created() {
let _this = this
this.uid = window.sessionStorage.getItem('UID')
if (this.uid === undefined || this.uid === 0) {
location.href = 'assort.html'
}
// 加载我的权限
this.loadUserPowerById()
// 加载所有权限
setTimeout(function () {
_this.loadAllPowers()
}, 1000)
}
})
</script>
问题1
只是显示该用户的权限,没有显示所有权限
解决:得到所有权限,对号选中的就是该用户权限
PowerController
//得到所有权限
@GetMapping("/findall")
public JsonResult find() {
return ResultTool.success(service.list());
}
又会出现一个问题,复选框没有打勾
解决:将powers的权限赋给checkedPower
问题2
线程错乱了
解决:
成功了
问题3
当点击修改权限的时候怎么修改?
解决:把所有权限全删了,再添加自己的权限
PowerController
@PutMapping("/update")
public JsonResult update(int uid, String[] checkedPower) {
log.info("uid:{}", uid);
log.info("checkedPower:{}", Arrays.toString(checkedPower));
return service.updatePowerByUserId(uid, checkedPower);
}
PowerService
public interface PowerService extends IService<Power> {
JsonResult findPowerByUserId(int id);
JsonResult updatePowerByUserId(int uid,String[] newPower);
}
RoleMapper
@Mapper
public interface PowerMapper extends BaseMapper<Power> {
@Select("select * from power where id in(select pid from userpower where uid=#{id}) or id in(select pid from rolepower where rid in (select rid from userrole where uid=#{id}))")
List<Power> findPowerByUserId(int id);
@Delete("delete from userpower where uid=#{id}")
void deletePowerByUserId(int id);
@Insert("insert into userpower(id,uid,pid) values(null,#{uid},#{pid})")
void savePower(@Param("uid") int uid, @Param("pid") String pid);
}
PowerServiceImpl
@Service
public class PowerServiceImpl extends ServiceImpl<PowerMapper, Power> implements PowerService {
@Resource
private PowerMapper powerMapper;
@Resource
private RoleMapper roleMapper;
@Override
public JsonResult findPowerByUserId(int id) {
return ResultTool.success(powerMapper.findPowerByUserId(id));
}
@Transactional(rollbackFor = SQLException.class)
public JsonResult updatePowerByUserId(int uid, String[] newPower) {
// 删除原来权限
powerMapper.deletePowerByUserId(uid);
roleMapper.deleteRoleByUserId(uid);
// 添加新权限
for (int i = 0; i < newPower.length; i++) {
String pid = newPower[i];
powerMapper.savePower(uid, pid);
}
return ResultTool.success("更新成功!");
}
}
发现修改成功