首页
关于
留言
统计
友链
壁纸
影视
留言找电影
Search
1
java jdk17版本使用cglib报错问题解决
320 阅读
2
在Java中修复json数据,json格式不正确,如何在Java中修复不正确的json
116 阅读
3
Java实现双链表插入排序~渡星河全网首发
106 阅读
4
Java打印某年某月的日历~不调用函数
76 阅读
5
Java中判断是否是闰年
70 阅读
代码笔记
Java
css
Javascript
数据库
安卓开发
Java 功能实例
Java 封装
我的日常
影音分享
文案
恋爱问答
资源分享
电脑游戏
实用APP
登录
/
注册
Search
标签搜索
java
java作业
恋爱
动漫
3A大作
独行月球
电影分享
总之就是非常可爱
JavaScript
office
破解
mysql
vue
java打包
跨域
tomcat解决乱码
bug解决
tomcat
vscode
渡星河
累计撰写
94
篇文章
累计收到
30
条评论
首页
栏目
代码笔记
Java
css
Javascript
数据库
安卓开发
Java 功能实例
Java 封装
我的日常
影音分享
文案
恋爱问答
资源分享
电脑游戏
实用APP
页面
关于
留言
统计
友链
壁纸
影视
留言找电影
搜索到
44
篇与
的结果
2024-01-30
Java实现将中文转换为拼音首字母
调用例子package com.zhkt.ostrich; /** * Author: duGalaxy * Date: 2023/09/11/11:03 * Description: */ import java.io.File; import static com.zhkt.ostrich.ChineseToLower.getPinYinHeadChar; public class test { public static void main(String[] args) { String directoryPath = "F:\\A公司项目\\汐游游戏\\20240126汐游二级02切图\\欢乐世界"; // 替换为你的目录路径 renameFiles(directoryPath); // int i = 2; // while (i<=20){ // System.out.println(directoryPath+i); // renameFiles(directoryPath+i); // i++; // } } public static void renameFiles(String directoryPath) { File directory = new File(directoryPath); File[] files = directory.listFiles(); int num = 1; String texy = ""; if (files != null) { for (File file : files) { if (file.isFile()) { String originalName = file.getName(); //讲包含病毒的文件名进行文件新命名 texy+=originalName+"\n"; String headChar = getPinYinHeadChar(originalName); String newName = headChar.toLowerCase(); // String newName; // String newName = originalName.replaceAll("欢乐世界", "").replaceAll("大", "").replaceAll("背景", "bg"); // newName = originalName.replaceAll(" ", ""); // newName = originalName.replaceAll("探险岛", "txd"); // String newName = num+".png"; num++; if (!originalName.equals(newName)) { String newPath = file.getParent() + File.separator + newName; File newFile = new File(newPath); if (file.renameTo(newFile)) { System.out.println("文件重命名成功:" + originalName + " --> " + newName); } else { System.out.println("文件重命名失败:" + originalName); } } } } } System.out.println(texy); } }工具类 package com.zhkt.ostrich; import java.util.regex.Matcher; import java.util.regex.Pattern; import net.sourceforge.pinyin4j.PinyinHelper; import net.sourceforge.pinyin4j.format.HanyuPinyinCaseType; import net.sourceforge.pinyin4j.format.HanyuPinyinOutputFormat; import net.sourceforge.pinyin4j.format.HanyuPinyinToneType; import net.sourceforge.pinyin4j.format.HanyuPinyinVCharType; import net.sourceforge.pinyin4j.format.exception.BadHanyuPinyinOutputFormatCombination; public class ChineseToLower { /** * 得到中文首字母 * @param str 需要转化的中文字符串 * @return */ public static String getPinYinHeadChar(String str) { String convert = ""; for (int j = 0; j < str.length(); j++) { char word = str.charAt(j); String[] pinyinArray = PinyinHelper.toHanyuPinyinStringArray(word); if (pinyinArray != null) { convert += pinyinArray[0].charAt(0); } else { convert += word; } } return convert; } /** * 得到中文全拼 * @param str 需要转化的中文字符串 * @return */ public static String getPingYin(String str) { char[] t1 = null; t1 = str.toCharArray(); String[] t2 = new String[t1.length]; HanyuPinyinOutputFormat t3 = new HanyuPinyinOutputFormat(); t3.setCaseType(HanyuPinyinCaseType.LOWERCASE); t3.setToneType(HanyuPinyinToneType.WITHOUT_TONE); t3.setVCharType(HanyuPinyinVCharType.WITH_V); String t4 = ""; int t0 = t1.length; try { for (int i = 0; i < t0; i++) { // 判断是否为汉字字符 if (java.lang.Character.toString(t1[i]).matches("[\\u4E00-\\u9FA5]+")) { t2 = PinyinHelper.toHanyuPinyinStringArray(t1[i], t3); t4 += t2[0]; } else { t4 += java.lang.Character.toString(t1[i]); } } return t4; } catch (BadHanyuPinyinOutputFormatCombination e1) { e1.printStackTrace(); } return t4; } /** * 获得中文字符串首字母 * @param str * @return */ public static String getFirstPinYin(String str){ String s = getPinYinHeadChar(str); StringBuffer sb = new StringBuffer(s); if (sb.length() > 1) { String ss = sb.delete(1, sb.length()).toString(); return String.valueOf(Character.toUpperCase(ss.toCharArray()[0])); } return str; } public static void main(String[] args) { //获取中文拼音首字母 String headChar = getPinYinHeadChar("欢乐世界-打我鸭.png"); // headChar.toUpperCase() String up = headChar.toLowerCase(); System.out.println(up); //获得中文拼音全拼 String pingYin = getPingYin("磊喆頔"); String s = pingYin.toUpperCase(); System.out.println(s); //获得中文拼音第一个字首字母 String ss = getFirstPinYin("湖北省"); System.out.println(ss); } }
2024年01月30日
9 阅读
0 评论
0 点赞
2023-12-20
springboot项目全局开启允许跨域
1.随便找个位置,必须和启动类同级,否则扫描不到import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration @EnableWebMvc public class WebConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("*") .allowedMethods("GET", "POST", "PUT", "DELETE") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }
2023年12月20日
10 阅读
0 评论
2 点赞
2023-11-29
springboot打包jar正常运行,打包war无法运行。打包的jar只有十几kb解决
解决方法启动类需要继承SpringBootServletInitializer并重写SpringApplicationBuilderpackage com.zhkt.ostrich; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; @SpringBootApplication public class OstrichApplication extends SpringBootServletInitializer { @Override protected SpringApplicationBuilder createSpringApplicationBuilder() { return super.createSpringApplicationBuilder(); } public static void main(String[] args) { SpringApplication.run(OstrichApplication.class, args); } } 2.maven做出调整,需要移除内置tomcat,因为war包是运行tomcat的会冲突<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> <scope>provided</scope> </dependency>3.重新打包即可正常在tomcat运行1.打包的jar只有几kb,使用java -jar xxx.jar提示没有清单文件//解决方法在pom文件调整<skip>false</skip> <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>${spring-boot.version}</version> <configuration> <mainClass>com.galaxy.jxactive.JxActiveApplication</mainClass> <skip>false</skip> </configuration> <executions> <execution> <id>repackage</id> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> </plugins>我的pom文件示例<?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.galaxy</groupId> <artifactId>jx-active</artifactId> <version>0.0.1-SNAPSHOT</version> <name>jx-active</name> <packaging>war</packaging> <description>jx-active</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.6.9</spring-boot.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.83</version> </dependency> <!-- mybatis-plus依賴 --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.2</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>3.4.1</version> </dependency> <dependency> <groupId>org.apache.velocity</groupId> <artifactId>velocity-engine-core</artifactId> <version>2.0</version> </dependency> <!-- <dependency>--> <!-- <groupId>io.springfox</groupId>--> <!-- <artifactId>springfox-boot-starter</artifactId>--> <!-- <version>3.0.0</version>--> <!-- </dependency>--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.8</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.12.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <scope>provided</scope> </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>${spring-boot.version}</version> <configuration> <mainClass>com.galaxy.jxactive.JxActiveApplication</mainClass> <skip>false</skip> </configuration> <executions> <execution> <id>repackage</id> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </project>
2023年11月29日
7 阅读
0 评论
1 点赞
2023-08-02
java实现批量修改文件名字,支持递归修改
今天美工给我发了一个文件夹,里面是图片,但是每个图片名字都不规范,一个个修改感觉麻烦,所以写了这个工具类package com.zhkt.ultraman; import java.io.File; public class test { public static void main(String[] args) { String directoryPath = "E:\\游戏开发\\放大镜找病毒\\游戏页面"; // 替换为你的目录路径 renameFiles(directoryPath); } public static void renameFiles(String directoryPath) { File directory = new File(directoryPath); File[] files = directory.listFiles(); if (files != null) { for (File file : files) { if (file.isFile()) { String originalName = file.getName(); //讲包含病毒的文件名进行文件新命名 String newName = originalName.replaceAll("病毒", ""); if (!originalName.equals(newName)) { String newPath = file.getParent() + File.separator + newName; File newFile = new File(newPath); if (file.renameTo(newFile)) { System.out.println("文件重命名成功:" + originalName + " --> " + newName); } else { System.out.println("文件重命名失败:" + originalName); } } } } } } }
2023年08月02日
28 阅读
0 评论
0 点赞
2023-05-13
springsecurity
一、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:用于缓存HttpServletRequestSecurityContextHolderAwareRequestFilter:用于封装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/loginPOST 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" 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和HttpSecuritySpring Security内部是如何加载我们自定义的登录页面的?需要了解这两个类:WebSecurity和HttpSecurity。WebSecurity在这个类里定义了一个securityFilterChainBuilders集合,可以同时管理多个SecurityFilterChain过滤器链,当WebSecurity在执行时,会构建出一个名为 ”springSecurityFilterChain“ 的 Spring BeanFilterChainProxy代理类,它的作用是来 定义哪些请求可以忽略安全控制,哪些请求必须接受安全控制;以及在合适的时候 清除SecurityContext 以避免内存泄漏,同时也可以用来 定义请求防火墙和请求拒绝处理器,也可以在这里 开启Spring Security 的Debug模式。HttpSecurityHttpSecurity用来构建包含一系列的过滤器链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提供了四个方法用于角色和权限的访问控制。通过这些方法,对用户是否具有某个或某些权限,进行过滤访问。对用户是否具备某个或某些角色,进行过滤访问。hasAuthorityhasAnyAuthorityhasRolehasAnyRole2.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(); } } 从数据库查询权限的Servicepackage 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.什么是JWTJSON Web Token (JWT)是一个开放的行业标准(RFC 7519),它定义了一种简洁的、自包含的协议格式,用于在通信双方传递json对象,传递的信息经过数字签名可以被验证和信任。JWT可以使用HMAC算法或使用RSA的公钥/私钥对进行签名,防止被篡改。JWT官网: https://jwt.ioJWT令牌的优点: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编码,编码后的字符串如下:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9Base64是一种基于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是自定义的私有声明,编码后如下:eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ3)签证、签名(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.使用JJWTJJWT是一个提供端到端的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 2021Token过期检验在有效期内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; } }
2023年05月13日
11 阅读
0 评论
0 点赞
1
2
3
...
9