一、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 请求,表示你想访问登录页面;如果是 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" style="padding-bottom: 20px">
<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;
}
}
评论 (0)