一、Spring Security简介
Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。Spring Security致力于为Java应用程序提供身份验证和授权的能力。像所有Spring项目一样,Spring Security的真正强大之处在于它可以轻松扩展以满足定制需求的能力。
Spring Security两大重要核心功能:用户认证(Authentication)和用户授权(Authorization)。
用户认证:验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。
用户授权:验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,有的用户既能读取,又能修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。
二、快速开始
使用Springboot工程搭建Spring Security项目。
1.引入依赖
在pom中新增了Spring Security的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2.创建测试访问接口
用于访问接口时触发Spring Security登陆页面
package com.qf.my.ss.demo.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* web controller
* @author Thor
* @公众号 Java架构栈
*/
@RestController
public class SecurityController {
@RequestMapping("/hello")
public String hello(){
return "hello security";
}
}
3.访问接口,自动跳转至Security登陆页面
访问add接口,讲自动跳转至Security的登陆页面
默认账号是: user
默认密码是:启动项目的控制台中输出的密码
三、Spring Security基础概念
在上一节中访问add接口,发现被Spring Security的登陆页面拦截,可以猜到这是触发了Security框架的过滤器。Spring Security本质上就是一个过滤器链。下面讲介绍Security框架的过滤器链。
1.过滤器链
WebAsyncManagerIntegrationFilter:将SecurityContext集成到Spring MVC中用于管理异步请求处理的WebAsyncManager中。
SecurityContextPersistenceFilter:在当前会话中填充SecurityContext,SecurityContext即Security的上下文对象,里面包含了当前用户的认证及权限信息等。
HeaderWriterFilter:向请求的Header中添加信息
CsrfFilter:用于防止CSRF(跨域请求伪造)攻击。Spring Security会对所有post请求验证是否包含系统生成的CSRF的信息,如果不包含则报错。
LogoutFilter:匹配URL为“/logout”的请求,清除认证信息,实现用户注销功能。
UsernamePasswordAuthenticationFilter:认证操作的过滤器,用于匹配URL为“/login”的POST请求做拦截,校验表单中的用户名和密码。
DefaultLoginPageGeneratingFilter:如果没有配置登陆页面,则生成默认的认证页面
DefaultLogoutPageGeneratingFilter:用于生成默认的退出页面
BasicAuthenticationFilter:用于Http基本认证,自动解析Http请求头中名为Authentication的内容,并获得内容中“basic”开头之后的信息。
RequestCacheAwareFilter:用于缓存HttpServletRequest
SecurityContextHolderAwareRequestFilter:用于封装ServletRequest,让ServletRequest具备更多功能。
AnonymousAuthenticationFilter:对于未登录情况下的处理,当SecurityContextHolder中认证信息为空,则会创建一个匿名用户存入到SecurityContextHolder中
SessionManagementFilter:限制同一用户开启多个会话
ExceptionTranslationFilter:异常过滤器,用来处理在认证授权过程中抛出异常。
FilterSecurityInterceptor:获取授权信息,根据SecurityContextHolder中存储的用户信息判断用户是否有权限访问
2.过滤器加载过程
Springboot在整合Spring Security项目时会自动配置DelegatingFilterProxy过滤器,若非Springboot工程,则需要手动配置该过滤器。
过滤器如何进行加载的?
结合上图和源码,Security在DelegatingFilterProxy的doFilter()调用了initDelegat()方法,在该方法中调用了WebApplicationContext的getBean()方法,该方法出发FilterChainProxy的doFilterInternal方法,用于获取过滤链中的所有过滤器并进行加载。
四、Spring Security的认证方式-基本认证
1.认证概念
所谓的认证,就是用来判断系统中是否存在某用户,并判断该用户的身份是否合法的过程,解决的其实是用户登录的问题。认证的存在,是为了保护系统中的隐私数据与资源,只有合法的用户才可以访问系统中的资源。
2.认证方式
在Spring Security中,常见的认证方式可以分为HTTP层面和表单层面,常见的认证方式如下:
HTTP基本认证
Form表单认证
HTTP摘要认证
3.基本认证
HTTP基本认证是在RFC2616标准中定义的一种认证模式,它以一种很简单的方式与用户进行交互。HTTP基本认证可以分为如下4个步骤:
客户端首先发起一个未携带认证信息的请求;
然后服务器端返回一个401 Unauthorized的响应信息,并在WWW-Authentication头部中说明认证形式:当进行HTTP基本认证时,WWW-Authentication会被设置为Basic realm=“被保护的页面”;
接下来客户端会收到这个401 Unauthorized响应信息,并弹出一个对话框,询问用户名和密码。当用户输入后,客户端会将用户名和密码使用冒号进行拼接并用Base64编码,然后将其放入到请求的Authorization头部并发送给服务器;
最后服务器端对客户端发来的信息进行解码得到用户名和密码,并对该信息进行校验判断是否正确,最终给客户端返回响应内容。
HTTP基本认证是一种无状态的认证方式,与表单认证相比,HTTP基本认证是一种基于HTTP层面的认证方式,无法携带Session信息,也就无法实现Remember-Me功能。另外,用户名和密码在传递时仅做了一次简单的Base64编码,几乎等同于以明文传输,极易被进行密码窃听和重放攻击。所以在实际开发中,很少会使用这种认证方式来进行安全校验。
基本认证的代码实现:
创建SecurityConfig配置类
package com.qf.my.ss.demo.config;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
* @author Thor
* @公众号 Java架构栈
*/
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
//1.配置基本认证方式
http.authorizeRequests()
.anyRequest()
.authenticated()
.and()
//开启basic认证
.httpBasic();
}
}
Basic认证详解
在未登录状态下访问目标资源时,查看响应头,可以看到WWW-Authenticate认证信息:WWW-Authenticate:Basic realm=“Realm”,其中WWW-Authenticate: 表示服务器告知浏览器进行代理认证工作。Basic: 表示认证类型为Basic认证。realm=“Realm”: 表示认证域名为Realm域。
根据401和以上响应头信息,浏览器会弹出一个对话框,要求输入 用户名/密码,Basic认证会将其拼接成 “用户名:密码” 格式,中间是一个冒号,并利用Base64编码成加密字符串xxx;然后在请求头中附加 Authorization: Basic xxx 信息,发送给后台认证;后台需要利用Base64来进行解码xxx,得到用户名和密码,再校验 用户名:密码 信息。如果认证错误,浏览器会保持弹框;如果认证成功,浏览器会缓存有效的Base64编码,在之后的请求中,浏览器都会在请求头中添加该有效编码。
五、Form表单认证
在SpringBoot开发环境中,只要我们添加了Spring Security的依赖包,就会自动实现表单认证。可以通过WebSecurityConfigurerAdapter提供的configure方法看到默认的认证方式就是表单认证
protected void configure(HttpSecurity http) throws Exception {
logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity).");
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin().and()
.httpBasic();
}
1.表单认证中的预置url和页面
默认的formLogin配置中,自动配置了一些url和页面:
/login(get): get请求时会跳转到这个页面,只要我们访问任意一个需要认证的请求时,都会跳转到这个登录界面。
/login(post): post请求时会触发这个接口,在登录页面点击登录时,默认的登录页面表单中的action就是关联这个login接口。
/login?error: 当用户名或密码错误时,会跳转到该页面。
/: 登录成功后,默认跳转到该页面,如果配置了index.html页面,则 ”/“ 会重定向到index.html页面,当然这个页面要由我们自己实现。
/logout: 注销页面。
/login?logout: 注销成功后跳转到的页面。由此可见,SpringSecurity默认有两个login,即登录页面和登录接口的地址都是 /login:
GET http://localhost:8080/login
POST http://localhost:8080/login如果是 GET 请求,表示你想访问登录页面;如果是 POST 请求,表示你想提交登录数据。
对于这几个URL接口,我们简单了解即可。
2.自定义认证页面
自定义登陆页面
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- 上述3个meta标签*必须*放在最前面,任何其他内容都*必须*跟随其后! -->
<title>Bootstrap 101 Template</title>
<!-- Bootstrap -->
<link href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css" rel="stylesheet">
<!-- HTML5 shim 和 Respond.js 是为了让 IE8 支持 HTML5 元素和媒体查询(media queries)功能 -->
<!-- 警告:通过 file:// 协议(就是直接将 html 页面拖拽到浏览器中)访问页面时 Respond.js 不起作用 -->
<!--[if lt IE 9]>
<script src="https://cdn.jsdelivr.cn/npm/html5shiv@3.7.3/dist/html5shiv.min.js"></script>
<script src="https://cdn.jsdelivr.cn/npm/respond.js@1.4.2/dest/respond.min.js"></script>
<![endif]-->
<style>
.login-top{
width: 600px;
height: 300px;
border: 1px solid #DCDFE6;
margin: 150px auto;
padding: 20px 50px 20px 30px;
border-radius: 20px;
box-shadow: 0px 0px 20px #DCDFE6;
}
</style>
</head>
<body>
<div class="login-top">
<div>
<h3>欢迎登陆</h3>
</div>
<form action="/login" method="post">
<div class="form-group">
<label for="inputUsername" class="col-sm-2 control-label">用户名</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="inputUsername" name="username" placeholder="用户名">
</div>
</div>
<div class="form-group">
<label for="inputPassword" class="col-sm-2 control-label">密码</label>
<div class="col-sm-10">
<input type="password" class="form-control" id="inputPassword" name="password" placeholder="密码">
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<div class="checkbox">
<label>
<input type="checkbox"> 记住我
</label>
</div>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" class="btn btn-default">登陆</button>
</div>
</div>
</form>
</div>
</body>
<!-- jQuery (Bootstrap 的所有 JavaScript 插件都依赖 jQuery,所以必须放在前边) -->
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.1/jquery.js"></script><!-- 加载 Bootstrap 的所有 JavaScript 插件。你也可以根据需要只加载单个插件。 -->
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.2.2/css/bootstrap-utilities.min.css" rel="stylesheet"></body>
</html>
自定义首页
自定义错误页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h3>用户名或密码错误</h3>
</body>
</html>
3.自定义配置项
package com.qf.my.ss.demo.config;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
* @author Thor
* @公众号 Java架构栈
*/
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring()
.antMatchers("/js/**","/css/**","/images/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//1.配置基本认证方式
http.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.formLogin()
.loginPage("/login.html")
.permitAll()
//指登录成功后,是否始终跳转到登录成功url。它默认为false
.defaultSuccessUrl("/index.html",true)
//post登录接口,登录验证由系统实现
.loginProcessingUrl("/login")
//用户密码错误跳转接口
.failureUrl("/error.html")
//要认证的用户参数名,默认username
.usernameParameter("username")
//要认证的密码参数名,默认password
.passwordParameter("password")
.and()
//配置注销
.logout()
//注销接口
.logoutUrl("/logout")
//注销成功后跳转到的接口
.logoutSuccessUrl("/login.html")
.permitAll()
//删除自定义的cookie
.deleteCookies("myCookie")
.and()
//注意:需禁用crsf防护功能,否则登录不成功
.csrf()
.disable();
}
}
4.WebSecurity和HttpSecurity
Spring Security内部是如何加载我们自定义的登录页面的?需要了解这两个类:WebSecurity和HttpSecurity。
WebSecurity
在这个类里定义了一个securityFilterChainBuilders集合,可以同时管理多个SecurityFilterChain过滤器链,
当WebSecurity在执行时,会构建出一个名为 ”springSecurityFilterChain“ 的 Spring BeanFilterChainProxy代理类,它的作用是来 定义哪些请求可以忽略安全控制,哪些请求必须接受安全控制;以及在合适的时候 清除SecurityContext 以避免内存泄漏,同时也可以用来 定义请求防火墙和请求拒绝处理器,也可以在这里 开启Spring Security 的Debug模式。
HttpSecurity
HttpSecurity用来构建包含一系列的过滤器链SecurityFilterChain,平常我们的配置就是围绕着这个SecurityFilterChain进行。
5.Http摘要认证
概念
HTTP摘要认证和HTTP基本认证一样,也是在RFC2616中定义的一种认证方式,它的出现是为了弥补HTTP基本认证存在的安全隐患,但该认证方式也并不是很安全。HTTP摘要认证会使用对通信双方来说都可知的口令进行校验,且最终以密文的形式来传输数据,所以相对于基本认证来说,稍微安全了一些。
**HTTP摘要认证与基本认证类似,基于简单的“挑战-回应”模型。**当我们发起一个未经认证的请求时,服务器会返回一个401回应,并给客户端返回与验证相关的参数,期待客户端依据这些参数继续做出回应,从而完成整个验证过程。
摘要认证核心参数
服务端给客户端返回的验证相关参数如下:
username: 用户名。
password: 用户密码。
realm: 认证域,由服务器返回。
opaque: 透传字符串,客户端应原样返回。
method: 请求的方法。
nonce: 由服务器生成的随机字符串,包含过期时间(默认过期时间300s)和密钥。
nc: 即nonce-count,指请求的次数,用于计数,防止重放攻击。qop被指定时,nc也必须被指定。
cnonce: 客户端发给服务器的随机字符串,qop被指定时,cnonce也必须被指定。
qop: 保护级别,客户端根据此参数指定摘要算法。若取值为 auth,则只进行身份验证;若取值为auth-int,则还需要校验内容完整性,默认的qop为auth。
uri: 请求的uri。
response: 客户端根据算法算出的摘要值,这个算法取决于qop。
algorithm: 摘要算法,目前仅支持MD5。
entity-body: 页面实体,非消息实体,仅在auth-int中支持。
通常服务器端返回的数据包括realm、opaque、nonce、qop等字段,如果客户端需要做出验证回应,就必须按照一定的算法得到一些新的数据并一起返回。在以上各种参数中,对服务器而言,最重要的字段是nonce;对客户端而言,最重要的字段是response。
摘要认证的实现
package com.qf.my.spring.security.demo.config;
import com.qf.my.spring.security.demo.service.MyUserDetailService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.authentication.www.DigestAuthenticationEntryPoint;
import org.springframework.security.web.authentication.www.DigestAuthenticationFilter;
/**
* 摘要认证的配置
* @author Thor
* @公众号 Java架构栈
*/
@EnableWebSecurity
public class DigestConfig extends WebSecurityConfigurerAdapter {
@Autowired
private DigestAuthenticationEntryPoint digestAuthenticationEntryPoint;
@Autowired
private MyUserDetailService userDetailService;
//配置认证入口端点,主要是设置认证参数信息
@Bean
public DigestAuthenticationEntryPoint digestAuthenticationEntryPoint(){
DigestAuthenticationEntryPoint point = new DigestAuthenticationEntryPoint();
point.setKey("security demo");
point.setRealmName("thor");
point.setNonceValiditySeconds(500);
return point;
}
public DigestAuthenticationFilter digestAuthenticationFilter(){
DigestAuthenticationFilter filter = new DigestAuthenticationFilter();
filter.setAuthenticationEntryPoint(digestAuthenticationEntryPoint);
filter.setUserDetailsService(userDetailService);
return filter;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/hello").hasAuthority("role")
.anyRequest().authenticated()
.and().csrf().disable()
//当未认证时访问某些资源,则由该认证入口类来处理.
.exceptionHandling()
.authenticationEntryPoint(digestAuthenticationEntryPoint)
.and()
//添加自定义过滤器到过滤器链中
.addFilter(digestAuthenticationFilter());
}
}
六、自定义用户名和密码
Spring Security提供了多种方式自定义用户名和密码。
1.使用application.properties
# 配置用户名
spring.security.user.name=qfadmin
# 配置密码
spring.security.user.password=123456
还需要向IOC容器里注入一个PasswordEncoder,用于生成密码的base64编码的字符串,和解析base64编码的字符串为实际密码内容。
@Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
2.通过创建配置类实现设置
将用户名和密码写在配置类里,虽然配置类中可以自己编写用户名和密码的代码,但因为它是配置类的缘故,不适合将从数据库中获取用户名和密码的业务代码写入到配置类中。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//用于密码的密文处理
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
//生成密文
String password = passwordEncoder.encode("123456");
//设置用户名和密码
auth.inMemoryAuthentication().withUser("qfAdmin").password(password).roles("admin");
}
@Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
3.编写自定义实现类(常用)
设计数据库表
-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
`id` int NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL,
`password` varchar(60) NOT NULL,
`nickname` varchar(255) DEFAULT NULL,
`headImgUrl` varchar(255) DEFAULT NULL,
`phone` varchar(11) DEFAULT NULL,
`telephone` varchar(30) DEFAULT NULL,
`email` varchar(50) DEFAULT NULL,
`birthday` date DEFAULT NULL,
`sex` tinyint(1) DEFAULT NULL,
`status` tinyint(1) NOT NULL DEFAULT '1',
`createTime` datetime NOT NULL,
`updateTime` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `username` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
SET FOREIGN_KEY_CHECKS = 1;
使用mybatis-generator生成映射文件
引入Mybatis和连接池的依赖
<!-- mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- druid连接-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<!-- mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
编写application.properties配置文件
# 指明mapper映射文件的位置
mybatis.mapper-locations=classpath:mapper/*.xml
# 配置连接池Druid
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/db_security?serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=qf123456
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
启动类上打上注解
@SpringBootApplication
@MapperScan("com.qf.my.ss.demo.mapper")
public class MySsDemoApplication {
public static void main(String[] args) {
SpringApplication.run(MySsDemoApplication.class, args);
}
}
编写UserDetailService实现类
编写从数据库中获取用户名和密码的业务
package com.qf.my.ss.demo.service;
import com.mysql.cj.util.StringUtils;
import com.qf.my.ss.demo.entity.SysUser;
import com.qf.my.ss.demo.mapper.SysUserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Objects;
/**
* @author Thor
* @公众号 Java架构栈
*/
@Service
public class MyUserDetailService implements UserDetailsService {
@Autowired
private SysUserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//设置角色,角色的概念在之后章节介绍
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("user");
//可以从数据库获取用户名和密码
if(StringUtils.isNullOrEmpty(username)){
return null;
}
SysUser sysUser = userMapper.selectByUsername(username);
User user = null;
if(Objects.nonNull(sysUser)){
user = new User(username,sysUser.getPassword(),auths);
}
return user;
}
}
编写SecurityConfig配置类,指明对UserDetailsService实现类认证
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
七、角色和权限
1.角色和权限的概念
所谓权限,就是用户是否有访问当前页面,或者是执行某个操作的权利。
所谓角色,是对权限的汇总,比如“管理员”角色,可以对数据进行增删改查,增删改查是数据的四个权限,拥有“管理员”角色的用户拥有这四个权限。“普通用户”角色,只具备数据的增和查两种权限,那么拥有“普通用户”角色的用户只拥有这两个权限。
Spring Security提供了四个方法用于角色和权限的访问控制。通过这些方法,对用户是否具有某个或某些权限,进行过滤访问。对用户是否具备某个或某些角色,进行过滤访问。
hasAuthority
hasAnyAuthority
hasRole
hasAnyRole
2.hasAuthority方法
判断当前主体是否有指定的权限,有返回true,否则返回false
该方法适用于只拥有一个权限的用户。
在配置类中设置当前主体具有怎样的权限才能访问。
package com.qf.my.ss.demo.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* @author Thor
* @公众号 Java架构栈
*/
@EnableWebSecurity
public class PermissionConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
//配置没有权限的跳转页面
http.exceptionHandling().accessDeniedPage("/nopermission.html");
http.formLogin()
.loginPage("/login.html") //设置自定义登陆页面
.loginProcessingUrl("/login") //登陆时访问的路径
.failureUrl("/error.html")//登陆失败的页面
.defaultSuccessUrl("/index.html").permitAll() //登陆成功后跳转的路径
.and().authorizeRequests()
.antMatchers("/","/login").permitAll() //设置可以直接访问的路径,取消拦截
//1.hasAuthority方法:当前登陆用户,只有具有admin权限才可以访问这个路径
.antMatchers("/index.html").hasAuthority("26")
.anyRequest().authenticated()
.and().csrf().disable(); //关闭csrf防护
}
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
从数据库查询权限的Service
package com.qf.my.ss.demo.service.impl;
import com.mysql.cj.util.StringUtils;
import com.qf.my.ss.demo.entity.SysUser;
import com.qf.my.ss.demo.mapper.SysRolePermissionMapper;
import com.qf.my.ss.demo.mapper.SysRoleUserMapper;
import com.qf.my.ss.demo.mapper.SysUserMapper;
import com.qf.my.ss.demo.service.PermissionService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.util.*;
/**
* @author Thor
* @公众号 Java架构栈
*/
@Service
public class PermissionServiceImpl implements PermissionService {
@Autowired
private SysRoleUserMapper roleUserMapper;
@Autowired
private SysRolePermissionMapper rolePermissionMapper;
@Autowired
private SysUserMapper userMapper;
@Override
public List<Integer> getPermissonsByName(String username) {
if(StringUtils.isNullOrEmpty(username)){
return null;
}
SysUser sysUser = userMapper.selectByUsername(username);
List<Integer> permissionIds = new ArrayList<>();
if(Objects.nonNull(sysUser)){
Integer id = sysUser.getId();
List<Integer> roleIds = roleUserMapper.selectByUserId(id);
if(!CollectionUtils.isEmpty(roleIds)){
//查询全选
roleIds.forEach(rid -> {
List<Integer> pIds = rolePermissionMapper.selectByRoleId(rid);
permissionIds.addAll(pIds);
});
//去重
Set<Integer> pSet = new HashSet<>(permissionIds);
permissionIds.clear();
permissionIds.addAll(pSet);
}
}
return permissionIds;
}
}
在userdetailsService,为返回的User对象设置权限
package com.qf.my.ss.demo.service;
import com.mysql.cj.util.StringUtils;
import com.qf.my.ss.demo.entity.SysUser;
import com.qf.my.ss.demo.mapper.SysUserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* @author Thor
* @公众号 Java架构栈
*/
@Service
public class MyUserDetailService implements UserDetailsService {
@Autowired
private SysUserMapper userMapper;
@Autowired
private PermissionService permissionService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if(StringUtils.isNullOrEmpty(username)){
return null;
}
//从数据库获得该用户相关的权限
List<Integer> permissons = permissionService.getPermissonsByName(username);
//设置权限
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList(
permissons.stream().map(String::valueOf).collect(Collectors.joining(",")));
SysUser sysUser = userMapper.selectByUsername(username);
User user = null;
if(Objects.nonNull(sysUser)){
user = new User(username,sysUser.getPassword(),auths);
}
return user;
}
}
3.hasAnyAuthority方法
适用于一个主体有多个权限的情况,多个权限用逗号隔开。
package com.qf.my.ss.demo.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* @author Thor
* @公众号 Java架构栈
*/
@EnableWebSecurity
public class PermissionConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
//配置没有权限的跳转页面
http.exceptionHandling().accessDeniedPage("/nopermission.html");
http.formLogin()
.loginPage("/login.html") //设置自定义登陆页面
.loginProcessingUrl("/login") //登陆时访问的路径
.failureUrl("/error.html")//登陆失败的页面
.defaultSuccessUrl("/index.html").permitAll() //登陆成功后跳转的路径
.and().authorizeRequests()
.antMatchers("/","/login").permitAll() //设置可以直接访问的路径,取消拦截
//1.hasAuthority方法:当前登陆用户,只有具有admin权限才可以访问这个路径
.antMatchers("/index.html").hasAnyAuthority("26,9")
.anyRequest().authenticated()
.and().csrf().disable(); //关闭csrf防护
}
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
4.hasRole方法
如果用户具备给定角色就允许访问,否则报403错误。
修改配置类
@Override
protected void configure(HttpSecurity http) throws Exception {
//配置没有权限的跳转页面
http.exceptionHandling().accessDeniedPage("/nopermission.html");
http.formLogin()
.loginPage("/login.html") //设置自定义登陆页面
.loginProcessingUrl("/login") //登陆时访问的路径
.failureUrl("/error.html")//登陆失败的页面
.defaultSuccessUrl("/index.html").permitAll() //登陆成功后跳转的路径
.and().authorizeRequests()
.antMatchers("/","/login").permitAll() //设置可以直接访问的路径,取消拦截
.antMatchers("/index.html").hasRole("1")
.anyRequest().authenticated()
.and().csrf().disable(); //关闭csrf防护
}
在PermissionServiceImpl添加获得角色的功能
@Override
public List<Integer> getRoleByName(SysUser sysUser) {
return roleUserMapper.selectByUserId(sysUser.getId());
}
修改UserDetailsService
//权限设置
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//根据用户输入的用户名去数据库查询具体的用户对象
if(StringUtils.isNullOrEmpty(username)){
return null;
}
//数据库查询
SysUser sysUser = userMapper.selectByUsername(username);
User user = null;
if(Objects.nonNull(sysUser)){
//从数据库获得该用户相关的权限
List<Integer> permissons = permissionService.getPermissonsByName(username);
String perString = permissons.stream().map(String::valueOf).collect(Collectors.joining(","));
//从数据库获得该用户的角色
SysUser sysUser = userMapper.selectByUsername(username);
List<Integer> roles = permissionService.getRoleByName(sysUser);
String roleString = roles.stream().map(num -> "ROLE_" + num).collect(Collectors.joining(","));
//设置权限
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList(perString+","+roleString);
user = new User(username,sysUser.getPassword(),auths);
}
return user;
}
其中角色student需要在设置时加上“ROLE_”前缀,因为通过源码hasRole方法给自定义的角色名前加上了“ROLE_”前缀
private static String hasRole(String role) {
Assert.notNull(role, "role cannot be null");
Assert.isTrue(!role.startsWith("ROLE_"), () -> {
return "role should not start with 'ROLE_' since it is automatically inserted. Got '" + role + "'";
});
return "hasRole('ROLE_" + role + "')";
}
5.hasAnyRole方法
设置多个角色,多个角色之间使用逗号隔开,只要用户具有某一个角色,就能访问。
@Override
protected void configure(HttpSecurity http) throws Exception {
//配置没有权限的跳转页面
http.exceptionHandling().accessDeniedPage("/nopermission.html");
http.formLogin()
.loginPage("/login.html") //设置自定义登陆页面
.loginProcessingUrl("/login") //登陆时访问的路径
.failureUrl("/error.html")//登陆失败的页面
.defaultSuccessUrl("/index.html").permitAll() //登陆成功后跳转的路径
.and().authorizeRequests()
.antMatchers("/","/login").permitAll() //设置可以直接访问的路径,取消拦截
.antMatchers("/index.html").hasAnyRole("1","2")
.anyRequest().authenticated()
.and().csrf().disable(); //关闭csrf防护
}
八、自动登陆
1. 准备数据库表
创建persistent_logins表,用于持久化自动登陆的信息。
create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null)
2.实现自动登陆
修改SecurityConfig配置类
package com.qf.my.ss.demo.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import javax.sql.DataSource;
/**
* @author Thor
* @公众号 Java架构栈
*/
@EnableWebSecurity
public class PermissionConfig extends WebSecurityConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Override
protected void configure(HttpSecurity http) throws Exception {
//配置数据源
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
//配置没有权限的跳转页面
http.exceptionHandling().accessDeniedPage("/nopermission.html");
http.formLogin()
.loginPage("/login.html") //设置自定义登陆页面
.loginProcessingUrl("/login") //登陆时访问的路径
.failureUrl("/error.html")//登陆失败的页面
.defaultSuccessUrl("/index.html").permitAll() //登陆成功后跳转的路径
.and().authorizeRequests()
.antMatchers("/","/login").permitAll()
.antMatchers("/index.html").hasRole("1")
.anyRequest().authenticated()
//开启记住我功能
.and().rememberMe().userDetailsService(userDetailsService)
//持久化令牌方案
.tokenRepository(tokenRepository)
//设置令牌有效期,为7天有效期
.tokenValiditySeconds(60*60*24*7)
.and().csrf().disable(); //关闭csrf防护
}
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
前端页面添加自动登陆表单项
<div class="checkbox">
<label>
<input type="checkbox" name="remember-me"> 记住我
</label>
</div>
3.自动登陆底层实现逻辑
首先从前端传来的 cookie 中解析出 series 和 token;
根据 series 从数据库中查询出一个 PersistentRememberMeToken 实例;
如果查出来的 token 和前端传来的 token 不相同,说明账号可能被人盗用(别人用你的令牌登录之后,token 会变)。此时根据用户名移除相关的 token,相当于必须要重新输入用户名密码登录才能获取新的自动登录权限。
接下来校验 token 是否过期;
构造新的 PersistentRememberMeToken 对象,并且更新数据库中的 token(这就是我们文章开头说的,新的会话都会对应一个新的 token);
将新的令牌重新添加到 cookie 中返回;
根据用户名查询用户信息,再走一波登录流程。
九、用户注销
1.在配置类添加注销的配置
@Override
protected void configure(HttpSecurity http) throws Exception {
//注销的配置
http.logout().logoutUrl("/logout") //注销时访问的路径
.logoutSuccessUrl("/logoutSuccess").permitAll(); //注销成功后访问的路径
//配置没有权限的跳转页面
http.exceptionHandling().accessDeniedPage("/error.html");
http.formLogin()
.loginPage("/login.html") //设置自定义登陆页面
.loginProcessingUrl("/usr/login") //登陆时访问的路径
// .defaultSuccessUrl("/index").permitAll() //登陆成功后跳转的路径
.defaultSuccessUrl("/success.html").permitAll() //登陆成功后跳转的路径
.and().authorizeRequests()
.antMatchers("/","/add","/user/login").permitAll() //设置可以直接访问的路径,取消拦截
//1.hasAuthority方法:当前登陆用户,只有具有admin权限才可以访问这个路径
//.antMatchers("/index").hasAuthority("admin")
//2.hasAnyAuthority方法:当前登陆用户,具有admin或manager权限可以访问这个路径
//.antMatchers("/index").hasAnyAuthority("admin,manager")
//3.hasRole方法:当前主体具有指定角色,则允许访问
//.antMatchers("/index").hasRole("student")
//4.hasAnyRole方法:当前主体只要具备其中某一个角色就能访问
.antMatchers("/index").hasAnyRole("student1,teacher")
.anyRequest().authenticated()
.and().csrf().disable(); //关闭csrf防护
}
2.设置注销链接
添加success.html页面作为登陆成功后的跳转页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
登陆成功 <a href="/logout">退出</a>
</body>
</html>
登陆后访问退出按钮,实现注销功能。
十、JWT(Json Web Token)
1.基于Token的认证方式
使用基于Token的身份验证方法,在服务端不需要存储用户的登陆信息。流程如下:
客户端使用用户名和密码请求登陆。
服务端收到请求,去验证用户名和密码。
验证成功后,服务端会签发一个Token,再把这个Token发送给客户端。
客户端收到Token以后可以把它存储在Cookie本地。
客户端每次向服务端请求资源时需要携带Cookie中该Token。
服务端收到请求后,验证客户端携带的Token,如果验证成功则返回数据。
2.什么是JWT
JSON Web Token (JWT)是一个开放的行业标准(RFC 7519),它定义了一种简洁的、自包含的协议格式,用于在通信双方传递json对象,传递的信息经过数字签名可以被验证和信任。JWT可以使用HMAC算法或使用RSA的公钥/私钥对进行签名,防止被篡改。
JWT官网: https://jwt.io
JWT令牌的优点:
JWT基于json,非常方便解析。
可以在令牌中自定义丰富的内容,易扩展。
通过非对称加密算法及数字签名技术,JWT防止篡改,安全性高。
资源服务使用JWT可不依赖认证服务即完成授权。JWT令牌的缺点:
JWT令牌较长,占存储空间比较大。
3.JWT组成
一个JWT实际上就一个字符串,它由三部分组成,头部、负载与签名。
1)头部(Header)
头部用于描述关于该JWT的最基本信息,例如其类型(即JWT)以及签名所用的算法(如HMAC SHA256 或 RSA)等。这也可以被表示成一个JSON对象。
{
"alg":"HS256",
"typ":"JWT"
}
alg:签名算法
typ:类型我们对头部的json字符串进行BASE64编码,编码后的字符串如下:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Base64是一种基于64个可打印字符串来表示二进制数据的表示方式。JDK提供了非常方便的Base64Encoder和Base64Decoder,用它们可以非常方便的完成基于Base64的编码和解码。
2)负载(Payload)
负载,是存放有效信息的地方,比如用户的基本信息可以存在该部分中。负载包含三个部分:
标准中注册的声明(建议但不强制使用)
iss:jwt签发者
sub:jwt所面向的用户
aud:接收jwt的一方
exp:jwt的过期时间,过期时间必须大于签发时间
nbf:定义在什么时间之前,该jwt都是不可用的
iat:jwt的签发时间
jti:jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。公共的声明
公共的声明可以添加任何信息,一般添加用户的相关信息或其他业务需要的必要信息,但不建议添加敏感信息,因为该部分在客户端可解密。
私有的声明
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
私有声明也就是自定义claim,用于存放自定义键值对。
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
其中sub是标准的声明,name是自定义的私有声明,编码后如下:
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
3)签证、签名(Signature)
jwt的第三部分是一个签证信息,由三部分组成:
Header(Base64编码后)
Payload(Base64编码后)
Secret(盐,必须保密)这个部分需要Base64加密后的header和base4加密后的payload使用.连接组成的字符串,然后通过header重声明的加密方式进行加盐Secret组合加密,然后就构成了JWT的第三部分——使用“qfjava”作为盐:
eZqdTo1mRMB-o7co1oAiTvNvumfCkt-1H-CdfNm78Cw
从官方工具中可以看到,三个部分组合出的完整字符串:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.eZqdTo1mRMB-o7co1oAiTvNvumfCkt-1H-CdfNm78Cw
注意:secret是保存在服务器端的,jwt在签发生成也是在服务器端的,secret就是用来进行jwt的签发和验证,所以,它就是服务器端的私钥,在任何场景都不应该泄漏。一旦客户端得知这个secret,那就意味着客户端是可以自我签发jwt了。
4.使用JJWT
JJWT是一个提供端到端的JWT创建和验证的开源Java库。也就是说使用JJWT能快速完成JWT的功能开发。
引入依赖
创建Springboot工程并引入jjwt依赖,pom.xml如下:
<!--jjwt-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>RELEASE</version>
</dependency>
创建Token
@Test
public void testCrtToken(){
//创建JWT对象
JwtBuilder builder = Jwts.builder().setId("1001")//设置负载内容
.setSubject("小明")
.setIssuedAt(new Date())//设置签发时间
.signWith(SignatureAlgorithm.HS256, "qfjava");//设置签名秘钥
//构建token
String token = builder.compact();
System.out.println(token);
}
JWT将用户信息转换成Token字符串,生成结果如下:
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMDAxIiwic3ViIjoi5bCP5piOIiwiaWF0IjoxNjE1MzY2MDEyfQ.2LNcw1v64TNQ96eCpWKvtAccBUA-cEVMDyJNMef-zu0
解析Token
通过JWT解析Token,获取Token中存放的用户信息,即生成Claims对象。
@Test
public void testParseToken(){
String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMDAxIiwic3ViIjoi5bCP5piOIiwiaWF0IjoxNjE1MzY2MDEyfQ.2LNcw1v64TNQ96eCpWKvtAccBUA-cEVMDyJNMef-zu0";
//解析Token,生成Claims对象,Token中存放的用户信息解析到了claims对象中
Claims claims = Jwts.parser().setSigningKey("qfjava").parseClaimsJws(token).getBody();
System.out.println("id:" + claims.getId());
System.out.println("subject:" + claims.getSubject());
System.out.println("IssuedAt:" + claims.getIssuedAt());
}
解析结果如下:
id:1001
subject:小明
IssuedAt:Wed Mar 10 16:46:52 CST 2021
Token过期检验
在有效期内Token可以正常读取,超过有效期则Token失效
@Test
public void testExpToken(){
long now = System.currentTimeMillis(); //当前时间
long exp = now + 1000 * 60; //过期时间为1分钟
JwtBuilder builder = Jwts.builder().setId("1001")
.setSubject("小明")
.setIssuedAt(new Date())
.signWith(SignatureAlgorithm.HS256, "qfjava")
.setExpiration(new Date(exp));//设置超时
}
自定义claims
除了使用官方api设置属性值,也可以添加自定义键值对。
@Test
public void testCustomClaims(){
long now = System.currentTimeMillis(); //当前时间
long exp = now + 1000 * 60; //过期时间为1分钟
JwtBuilder builder = Jwts.builder().setId("1001")
.setSubject("小明")
.setIssuedAt(new Date())
.signWith(SignatureAlgorithm.HS256, "qfjava")
.setExpiration(new Date(exp))
.claim("role", "admin");//设置自定义键值对
}
使用下面语句获取属性值:
claims.get("role")
十一、微服务项目-使用Security+JWT实现权限管理
1.前后端分离的权限管理
2.引入依赖
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--spring security-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--druid连接-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<!--jjwt-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>RELEASE</version>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
3.登陆过滤器的实现
package com.qf.my.security.admin.demo.filter;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.qf.my.security.admin.demo.common.ResponseUtil;
import com.qf.my.security.admin.demo.common.ResultModel;
import com.qf.my.security.admin.demo.entity.SecurityUser;
import com.qf.my.security.admin.demo.entity.User;
import com.qf.my.security.admin.demo.security.TokenManager;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
/**
* @author Thor
* @公众号 Java架构栈
*/
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {
private TokenManager tokenManager;
private RedisTemplate redisTemplate;
private AuthenticationManager authenticationManager;
public TokenLoginFilter(AuthenticationManager authenticationManager,TokenManager tokenManager, RedisTemplate redisTemplate) {
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
this.authenticationManager = authenticationManager;
//不是只允许post请求,经过这个filter
this.setPostOnly(false);
//设置登陆的路径和请求方式
this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/user/login","POST"));
}
/**
* 执行认证的方法
* @param request
* @param response
* @return
* @throws AuthenticationException
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
//获取表单提供的数据
ObjectMapper objectMapper = new ObjectMapper();
try {
User user = objectMapper.readValue(request.getInputStream(), User.class);
//校验==认证的过程
Authentication authenticate = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword()
, new ArrayList<>())
);
return authenticate;
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException("认证失败");
}
}
/**
* 认证成功以后调用的方法
* @param request
* @param response
* @param chain
* @param authResult
* @throws IOException
* @throws ServletException
*/
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
//得到用户名
SecurityUser securityUser = (SecurityUser) authResult.getPrincipal();
String username = securityUser.getUsername();
//生成token
String token = tokenManager.crtToken(username);
//存入到redis username: 权限
redisTemplate.opsForValue().set(username,securityUser.getPermissionValueList());
//返回token
ResponseUtil.out(response, ResultModel.success(token));
}
/**
* 认证失败调用的方法
* @param request
* @param response
* @param failed
* @throws IOException
* @throws ServletException
*/
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
ResponseUtil.out(response,ResultModel.error(401,failed.getMessage()));
}
}
4.权限过滤器的实现
package com.qf.my.security.admin.demo.filter;
import com.mysql.cj.util.StringUtils;
import com.qf.my.security.admin.demo.security.TokenManager;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.util.CollectionUtils;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
/**
* @author Thor
* @公众号 Java架构栈
*/
public class TokenAuthFilter extends BasicAuthenticationFilter {
private TokenManager tokenManager;
private RedisTemplate redisTemplate;
public TokenAuthFilter(AuthenticationManager authenticationManager,
TokenManager tokenManager,RedisTemplate redisTemplate) {
super(authenticationManager);
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
}
/**
* 权限相关的操作
* @param request
* @param response
* @param chain
* @throws IOException
* @throws ServletException
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
//获得token
String token = request.getHeader("token");
if(!StringUtils.isNullOrEmpty(token)){
//使用jwt解析token获得username
String username = tokenManager.getUsernameFromToken(token);
//从redis中获得该用户名对应的权限
List<String> permissionValueList = (List<String>) redisTemplate.opsForValue().get(username);
//将取出的权限存入到权限上下文中,表示当前token对应的用户具备哪些权限
Collection<GrantedAuthority> authorityCollection = new ArrayList<>();
if(!CollectionUtils.isEmpty(permissionValueList)){
for (String permissionValue : permissionValueList) {
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permissionValue);
authorityCollection.add(authority);
}
}
//生成权限信息对象
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username,token,authorityCollection);
//把权限信息对象存入到权限上下文中
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
//放行
chain.doFilter(request,response);
}
}
5.注销处理器的实现
package com.qf.my.security.admin.demo.security;
import com.mysql.cj.util.StringUtils;
import com.qf.my.security.admin.demo.common.ResponseUtil;
import com.qf.my.security.admin.demo.common.ResultModel;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @author Thor
* @公众号 Java架构栈
*/
public class TokenLogoutHandler implements LogoutHandler {
private TokenManager tokenManager;
private RedisTemplate redisTemplate;
public TokenLogoutHandler(TokenManager tokenManager, RedisTemplate redisTemplate) {
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
}
/**
* 注销时具体要执行的业务
* @param request
* @param response
* @param authentication
*/
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
//1.从请求头中获得前端携带的token
String token = request.getHeader("token");
if(!StringUtils.isNullOrEmpty(token)){
//2.使用jwt解析token
String username = tokenManager.getUsernameFromToken(token);
//3.删除redis中的数据
redisTemplate.delete(username);
}
ResponseUtil.out(response, ResultModel.success("注销成功"));
}
}
6.用户名密码验证逻辑
package com.qf.my.security.admin.demo.service.impl;
import com.qf.my.security.admin.demo.entity.SecurityUser;
import com.qf.my.security.admin.demo.entity.User;
import com.qf.my.security.admin.demo.service.PermissionService;
import com.qf.my.security.admin.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Objects;
/**
* @author Thor
* @公众号 Java架构栈
*/
@Service("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserService userService;
@Autowired
private PermissionService permissionService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//根据用户名从数据库查询到该用户的信息
User user = userService.selectByUsername(username);
if(Objects.isNull(user)) {
throw new UsernameNotFoundException("当前用户不存在");
}
//根据用户名从数据库查询到该用户的权限信息
List<String> permissionValues = permissionService.selectPermissionValueByUserId(user.getId());
SecurityUser securityUser = new SecurityUser();
securityUser.setCurrentUserInfo(user);
securityUser.setPermissionValueList(permissionValues);
return securityUser;
}
}
7.其他相关代码
package com.qf.my.security.admin.demo.common;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author Thor
* @公众号 Java架构栈
*/
public class ResponseUtil {
public static void out(HttpServletResponse response,ResultModel resultModel){
ObjectMapper objectMapper = new ObjectMapper();
//封装response的状态码和内容格式
response.setStatus(HttpStatus.OK.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
//内容:resultModel json
try {
//使用jackson,把json格式的resultModel写入到response的输出流中
objectMapper.writeValue(response.getOutputStream(),resultModel);
} catch (IOException e) {
e.printStackTrace();
}
}
}
package com.qf.my.security.admin.demo.common;
import lombok.Data;
import java.io.Serializable;
/**
* 用于封装返回给前端的信息
* @author Thor
* @公众号 Java架构栈
*/
@Data
public class ResultModel<T> implements Serializable {
//状态码
private int code; //1000表示成功 401表示认证失败
//消息
private String message;
//数据
private T result;
private static ResultModel resultModel = new ResultModel();
public static ResultModel success(){
resultModel.setCode(1000);
resultModel.setMessage("success");
resultModel.setResult(null);
return resultModel;
}
public static ResultModel success(String message){
resultModel.setCode(1000);
resultModel.setMessage(message);
resultModel.setResult(null);
return resultModel;
}
public static ResultModel success(Object data){
resultModel.setCode(1000);
resultModel.setMessage("success");
resultModel.setResult(data);
return resultModel;
}
public static ResultModel success(String message,Object data){
resultModel.setCode(1000);
resultModel.setMessage(message);
resultModel.setResult(data);
return resultModel;
}
public static ResultModel error(){
resultModel.setCode(500);
resultModel.setMessage("error");
resultModel.setResult(null);
return resultModel;
}
public static ResultModel error(int code,String message){
resultModel.setCode(code);
resultModel.setMessage(message);
resultModel.setResult(null);
return resultModel;
}
}
package com.qf.my.security.admin.demo.config;
import com.qf.my.security.admin.demo.filter.TokenAuthFilter;
import com.qf.my.security.admin.demo.filter.TokenLoginFilter;
import com.qf.my.security.admin.demo.security.TokenLogoutHandler;
import com.qf.my.security.admin.demo.security.TokenManager;
import com.qf.my.security.admin.demo.security.UnAuthEntryPoint;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* @author Thor
* @公众号 Java架构栈
*/
@Configurable
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class TokenSecurityConfig extends WebSecurityConfigurerAdapter {
private TokenManager tokenManager;
private RedisTemplate redisTemplate;
private UserDetailsService userDetailsService;
@Autowired
public TokenSecurityConfig(TokenManager tokenManager, RedisTemplate redisTemplate, UserDetailsService userDetailsService) {
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
this.userDetailsService = userDetailsService;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling()
//没有权限时的处理方案
.authenticationEntryPoint(new UnAuthEntryPoint())
//关闭csrf
.and().csrf().disable()
//要求所有接口都需要做权限认证
.authorizeRequests()
.anyRequest().authenticated()
//注销路径
.and().logout().logoutUrl("/user/logout")
//注销的处理器: 准备专门操作token的类 + redisTemplate
.addLogoutHandler(new TokenLogoutHandler(tokenManager,redisTemplate))
.and()
//处理登陆的逻辑
.addFilter(new TokenLoginFilter(authenticationManager(),tokenManager,redisTemplate))
//处理权限的filter
.addFilter(new TokenAuthFilter(authenticationManager(),tokenManager,redisTemplate))
.httpBasic();
}
@Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
/**
* 获得用户名和密码的方式
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
}
package com.qf.my.security.admin.demo.controller;
import com.qf.my.security.admin.demo.common.ResultModel;
import com.qf.my.security.admin.demo.security.TokenManager;
import com.qf.my.security.admin.demo.service.PermissionService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
/**
* @author Thor
* @公众号 Java架构栈
*/
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private PermissionService permissionService;
@GetMapping("/info")
public ResultModel<String> info(){
String username = SecurityContextHolder.getContext().getAuthentication().getName();
return ResultModel.success(username);
}
//携带token,获得该用户的所有权限,然后再在前端页面显示对应权限的内容
@GetMapping("/permission")
public ResultModel<List<String>> getPermissions(HttpServletRequest request){
String token = request.getHeader("token");
List<String> permissionValueList = permissionService.getPermissionsByToken(token);
return ResultModel.success(permissionValueList);
}
}
package com.qf.my.security.admin.demo.entity;
import com.mysql.cj.util.StringUtils;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* @author Thor
* @公众号 Java架构栈
*/
public class SecurityUser implements UserDetails {
//当前登陆的用户
private User currentUserInfo;
//存放权限列表
private List<String> permissionValueList;
public User getCurrentUserInfo() {
return currentUserInfo;
}
public void setCurrentUserInfo(User currentUserInfo) {
this.currentUserInfo = currentUserInfo;
}
public List<String> getPermissionValueList() {
return permissionValueList;
}
public void setPermissionValueList(List<String> permissionValueList) {
this.permissionValueList = permissionValueList;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
for (String permissionValue : permissionValueList) {
if(!StringUtils.isNullOrEmpty(permissionValue)){
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permissionValue);
authorities.add(authority);
}
}
return authorities;
}
@Override
public String getPassword() {
return this.currentUserInfo.getPassword();
}
@Override
public String getUsername() {
return this.currentUserInfo.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
package com.qf.my.security.admin.demo.entity;
public class SysRolePermissionKey {
private Integer roleid;
private Integer permissionid;
public Integer getRoleid() {
return roleid;
}
public void setRoleid(Integer roleid) {
this.roleid = roleid;
}
public Integer getPermissionid() {
return permissionid;
}
public void setPermissionid(Integer permissionid) {
this.permissionid = permissionid;
}
}
package com.qf.my.security.admin.demo.entity;
import java.util.Date;
public class SysUser {
private Integer id;
private String username;
private String password;
private String nickname;
private String headimgurl;
private String phone;
private String telephone;
private String email;
private Date birthday;
private Boolean sex;
private Boolean status;
private Date createtime;
private Date updatetime;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username == null ? null : username.trim();
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password == null ? null : password.trim();
}
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname == null ? null : nickname.trim();
}
public String getHeadimgurl() {
return headimgurl;
}
public void setHeadimgurl(String headimgurl) {
this.headimgurl = headimgurl == null ? null : headimgurl.trim();
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone == null ? null : phone.trim();
}
public String getTelephone() {
return telephone;
}
public void setTelephone(String telephone) {
this.telephone = telephone == null ? null : telephone.trim();
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email == null ? null : email.trim();
}
public Date getBirthday() {
return birthday;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
public Boolean getSex() {
return sex;
}
public void setSex(Boolean sex) {
this.sex = sex;
}
public Boolean getStatus() {
return status;
}
public void setStatus(Boolean status) {
this.status = status;
}
public Date getCreatetime() {
return createtime;
}
public void setCreatetime(Date createtime) {
this.createtime = createtime;
}
public Date getUpdatetime() {
return updatetime;
}
public void setUpdatetime(Date updatetime) {
this.updatetime = updatetime;
}
}
package com.qf.my.security.admin.demo.entity;
import lombok.Data;
import java.io.Serializable;
/**
* @author Thor
* @公众号 Java架构栈
*/
@Data
public class User implements Serializable {
private Integer id;
private String username;
private String password;
private String token;
}
package com.qf.my.security.admin.demo.mapper;
import java.util.List;
public interface PermissionMapper {
//根据用户id查询出该用户的所有权限
List<String> selectPermissionValuesByUserId(int userId);
}
package com.qf.my.security.admin.demo.mapper;
import com.qf.my.security.admin.demo.entity.SysUser;
public interface SysUserMapper {
int deleteByPrimaryKey(Integer id);
int insert(SysUser record);
int insertSelective(SysUser record);
SysUser selectByPrimaryKey(Integer id);
int updateByPrimaryKeySelective(SysUser record);
int updateByPrimaryKey(SysUser record);
SysUser selectByUsername(String username);
}
package com.qf.my.security.admin.demo.security;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
* @author Thor
* @公众号 Java架构栈
*/
@Component
public class TokenManager {
//设置token的私钥
@Value("{token.securityKey}")
private String securityKey;
//过期时间为7天
private long exp = 60*60*24*7*1000;
/**
* 使用jwt生成token
* @param username
* @return
*/
public String crtToken(String username){
String token = Jwts.builder().setSubject(username)
//设置过期时间
.setExpiration(new Date(System.currentTimeMillis() + exp))
.signWith(SignatureAlgorithm.HS256, securityKey).compact();
return token;
}
/**
* 用jwt解析token获得用户名
* @param token
* @return
*/
public String getUsernameFromToken(String token) {
Claims claims = Jwts.parser().setSigningKey(securityKey).parseClaimsJws(token).getBody();
return claims.getSubject();
}
}
package com.qf.my.security.admin.demo.security;
import com.qf.my.security.admin.demo.common.ResponseUtil;
import com.qf.my.security.admin.demo.common.ResultModel;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author Thor
* @公众号 Java架构栈
*/
public class UnAuthEntryPoint implements AuthenticationEntryPoint {
/**
* 权限认证失败执行的方法
* @param request
* @param response
* @param authException
* @throws IOException
* @throws ServletException
*/
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
//ResultModel:状态码,信息,数据
ResponseUtil.out(response,ResultModel.error());
}
}
package com.qf.my.security.admin.demo.service.impl;
import com.qf.my.security.admin.demo.mapper.PermissionMapper;
import com.qf.my.security.admin.demo.security.TokenManager;
import com.qf.my.security.admin.demo.service.PermissionService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @author Thor
* @公众号 Java架构栈
*/
@Service
public class PermissionServiceImpl implements PermissionService {
@Autowired
private PermissionMapper permissionMapper;
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private TokenManager tokenManager;
@Override
public List<String> selectPermissionValueByUserId(Integer id) {
List<String> permissionValues = permissionMapper.selectPermissionValuesByUserId(id);
return permissionValues;
}
/**
* 根据token从redis中获得用户的所有权限
* @param token
* @return
*/
@Override
public List<String> getPermissionsByToken(String token) {
String username = tokenManager.getUsernameFromToken(token);
List<String> permissionValueList = (List<String>) redisTemplate.opsForValue().get(username);
return permissionValueList;
}
}
package com.qf.my.security.admin.demo.service.impl;
import com.qf.my.security.admin.demo.entity.SecurityUser;
import com.qf.my.security.admin.demo.entity.User;
import com.qf.my.security.admin.demo.service.PermissionService;
import com.qf.my.security.admin.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Objects;
/**
* @author Thor
* @公众号 Java架构栈
*/
@Service("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserService userService;
@Autowired
private PermissionService permissionService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//根据用户名从数据库查询到该用户的信息
User user = userService.selectByUsername(username);
if(Objects.isNull(user)) {
throw new UsernameNotFoundException("当前用户不存在");
}
//根据用户名从数据库查询到该用户的权限信息
List<String> permissionValues = permissionService.selectPermissionValueByUserId(user.getId());
SecurityUser securityUser = new SecurityUser();
securityUser.setCurrentUserInfo(user);
securityUser.setPermissionValueList(permissionValues);
return securityUser;
}
}
package com.qf.my.security.admin.demo.service.impl;
import com.qf.my.security.admin.demo.entity.SysUser;
import com.qf.my.security.admin.demo.entity.User;
import com.qf.my.security.admin.demo.mapper.SysUserMapper;
import com.qf.my.security.admin.demo.service.UserService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Objects;
/**
* @author Thor
* @公众号 Java架构栈
*/
@Service
public class UserServiceImpl implements UserService {
@Autowired
private SysUserMapper userMapper;
@Override
public User selectByUsername(String username) {
SysUser sysUser = userMapper.selectByUsername(username);
User user = new User();
if(Objects.nonNull(sysUser)){
BeanUtils.copyProperties(sysUser,user);
}
return user;
}
}
package com.qf.my.security.admin.demo.service;
import java.util.List;
/**
* @author Thor
* @公众号 Java架构栈
*/
public interface PermissionService {
List<String> selectPermissionValueByUserId(Integer id);
List<String> getPermissionsByToken(String token);
}
package com.qf.my.security.admin.demo.service;
import com.qf.my.security.admin.demo.entity.User;
/**
* @author Thor
* @公众号 Java架构栈
*/
public interface UserService {
User selectByUsername(String username);
}
package com.qf.my.security.admin.demo;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("com.qf.my.security.admin.demo.mapper")
public class MySecurityAdminDemoApplication {
public static void main(String[] args) {
SpringApplication.run(MySecurityAdminDemoApplication.class, args);
}
}
<?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.qf.my.security.admin.demo.mapper.PermissionMapper">
<resultMap type="com.qf.my.security.admin.demo.entity.SysRolePermissionKey">
<id column="roleId" jdbcType="INTEGER" property="roleid" />
<id column="permissionId" jdbcType="INTEGER" property="permissionid" />
</resultMap>
<select parameterType="java.lang.Integer" resultType="java.lang.String">
select srp.permissionId
from sys_role_user sru
inner join sys_role_permission srp
on sru.roleId = srp.roleId
where sru.userId = #{userId,jdbcType=INTEGER}
</select>
</mapper>
<?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.qf.my.security.admin.demo.mapper.SysUserMapper">
<resultMap type="com.qf.my.security.admin.demo.entity.SysUser">
<id column="id" jdbcType="INTEGER" property="id" />
<result column="username" jdbcType="VARCHAR" property="username" />
<result column="password" jdbcType="VARCHAR" property="password" />
<result column="nickname" jdbcType="VARCHAR" property="nickname" />
<result column="headImgUrl" jdbcType="VARCHAR" property="headimgurl" />
<result column="phone" jdbcType="VARCHAR" property="phone" />
<result column="telephone" jdbcType="VARCHAR" property="telephone" />
<result column="email" jdbcType="VARCHAR" property="email" />
<result column="birthday" jdbcType="DATE" property="birthday" />
<result column="sex" jdbcType="BIT" property="sex" />
<result column="status" jdbcType="BIT" property="status" />
<result column="createTime" jdbcType="TIMESTAMP" property="createtime" />
<result column="updateTime" jdbcType="TIMESTAMP" property="updatetime" />
</resultMap>
<sql>
id, username, password, nickname, headImgUrl, phone, telephone, email, birthday,
sex, status, createTime, updateTime
</sql>
<select parameterType="java.lang.Integer" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from sys_user
where id = #{id,jdbcType=INTEGER}
</select>
<select parameterType="java.lang.String" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from sys_user
where username = #{username,jdbcType=VARCHAR}
</select>
<delete parameterType="java.lang.Integer">
delete from sys_user
where id = #{id,jdbcType=INTEGER}
</delete>
<insert parameterType="com.qf.my.security.admin.demo.entity.SysUser">
insert into sys_user (id, username, password,
nickname, headImgUrl, phone,
telephone, email, birthday,
sex, status, createTime,
updateTime)
values (#{id,jdbcType=INTEGER}, #{username,jdbcType=VARCHAR}, #{password,jdbcType=VARCHAR},
#{nickname,jdbcType=VARCHAR}, #{headimgurl,jdbcType=VARCHAR}, #{phone,jdbcType=VARCHAR},
#{telephone,jdbcType=VARCHAR}, #{email,jdbcType=VARCHAR}, #{birthday,jdbcType=DATE},
#{sex,jdbcType=BIT}, #{status,jdbcType=BIT}, #{createtime,jdbcType=TIMESTAMP},
#{updatetime,jdbcType=TIMESTAMP})
</insert>
<insert parameterType="com.qf.my.security.admin.demo.entity.SysUser">
insert into sys_user
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="id != null">
id,
</if>
<if test="username != null">
username,
</if>
<if test="password != null">
password,
</if>
<if test="nickname != null">
nickname,
</if>
<if test="headimgurl != null">
headImgUrl,
</if>
<if test="phone != null">
phone,
</if>
<if test="telephone != null">
telephone,
</if>
<if test="email != null">
email,
</if>
<if test="birthday != null">
birthday,
</if>
<if test="sex != null">
sex,
</if>
<if test="status != null">
status,
</if>
<if test="createtime != null">
createTime,
</if>
<if test="updatetime != null">
updateTime,
</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="id != null">
#{id,jdbcType=INTEGER},
</if>
<if test="username != null">
#{username,jdbcType=VARCHAR},
</if>
<if test="password != null">
#{password,jdbcType=VARCHAR},
</if>
<if test="nickname != null">
#{nickname,jdbcType=VARCHAR},
</if>
<if test="headimgurl != null">
#{headimgurl,jdbcType=VARCHAR},
</if>
<if test="phone != null">
#{phone,jdbcType=VARCHAR},
</if>
<if test="telephone != null">
#{telephone,jdbcType=VARCHAR},
</if>
<if test="email != null">
#{email,jdbcType=VARCHAR},
</if>
<if test="birthday != null">
#{birthday,jdbcType=DATE},
</if>
<if test="sex != null">
#{sex,jdbcType=BIT},
</if>
<if test="status != null">
#{status,jdbcType=BIT},
</if>
<if test="createtime != null">
#{createtime,jdbcType=TIMESTAMP},
</if>
<if test="updatetime != null">
#{updatetime,jdbcType=TIMESTAMP},
</if>
</trim>
</insert>
<update parameterType="com.qf.my.security.admin.demo.entity.SysUser">
update sys_user
<set>
<if test="username != null">
username = #{username,jdbcType=VARCHAR},
</if>
<if test="password != null">
password = #{password,jdbcType=VARCHAR},
</if>
<if test="nickname != null">
nickname = #{nickname,jdbcType=VARCHAR},
</if>
<if test="headimgurl != null">
headImgUrl = #{headimgurl,jdbcType=VARCHAR},
</if>
<if test="phone != null">
phone = #{phone,jdbcType=VARCHAR},
</if>
<if test="telephone != null">
telephone = #{telephone,jdbcType=VARCHAR},
</if>
<if test="email != null">
email = #{email,jdbcType=VARCHAR},
</if>
<if test="birthday != null">
birthday = #{birthday,jdbcType=DATE},
</if>
<if test="sex != null">
sex = #{sex,jdbcType=BIT},
</if>
<if test="status != null">
status = #{status,jdbcType=BIT},
</if>
<if test="createtime != null">
createTime = #{createtime,jdbcType=TIMESTAMP},
</if>
<if test="updatetime != null">
updateTime = #{updatetime,jdbcType=TIMESTAMP},
</if>
</set>
where id = #{id,jdbcType=INTEGER}
</update>
<update parameterType="com.qf.my.security.admin.demo.entity.SysUser">
update sys_user
set username = #{username,jdbcType=VARCHAR},
password = #{password,jdbcType=VARCHAR},
nickname = #{nickname,jdbcType=VARCHAR},
headImgUrl = #{headimgurl,jdbcType=VARCHAR},
phone = #{phone,jdbcType=VARCHAR},
telephone = #{telephone,jdbcType=VARCHAR},
email = #{email,jdbcType=VARCHAR},
birthday = #{birthday,jdbcType=DATE},
sex = #{sex,jdbcType=BIT},
status = #{status,jdbcType=BIT},
createTime = #{createtime,jdbcType=TIMESTAMP},
updateTime = #{updatetime,jdbcType=TIMESTAMP}
where id = #{id,jdbcType=INTEGER}
</update>
</mapper>
# 应用åç§°
spring.application.name=my-security-admin-demo
# 应用æœåŠ¡ WEB 访问端å£
server.port=8080
# tokençš„ç§é’¥
token.securityKey=qfjava
# mapperæ˜ å°„æ–‡ä»¶çš„ä½ç½®
mybatis.mapper-locations=classpath:mapper/*.xml
# é
置连接æ±
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/db_security?serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=qf123456
# redis
spring.redis.host=172.16.253.2
spring.redis.port=6379
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.qf</groupId>
<artifactId>my-security-admin-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>my-security-admin-demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.3.7.RELEASE</spring-boot.version>
</properties>
<dependencies>
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--spring security-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--druid连接-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<!--jjwt-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>RELEASE</version>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<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>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.3.7.RELEASE</version>
<configuration>
<mainClass>com.qf.my.security.admin.demo.MySecurityAdminDemoApplication</mainClass>
</configuration>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
[若依登录权限部分](https://gitee.com/y_project/RuoYi-Vue)
暂无评论内容