文章目录
一、入门案例
1.1、创建项目
使用Eclipse或者Idea等开发工具创建一个Spring Boot项目
1.2、引入依赖
<?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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.xxx</groupId>
<artifactId>spring_security_demo01</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring_security_demo01</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- security组件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- web组件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
1.3、编写代码
在src/main/resources/static
目录下编写一个或者几个html页面(当然不写也没问题);
在src/main/java目录下面创建com.xxx.xxx.controller
包,用来编写Controller代码的,也就是在这个包下创建Controller类,然后编写一个或者几个接口Api。
上面的两个操作至少要操作其中一个;两个步骤都操作一下都行。
1.4、测试效果
二、自定义登录逻辑
在入门demo中内置的登录页面是不符合实际项目开发需要的;而且用户名是固定的,密码是项目启动时自动生成的,这些都是不符合实际项目开发所需要的。
在实际的项目开发中往往都是要特别定制过的登录页面,而且登录的时候都是需要去数据库查询用户名和密码而不是用他默认的用户名和生成的密码。
2.1、认识登录的过程
认识登录的过程,需要了解和认识UserDetailsService
、UserDetails
、PasswordEncoder
这3个接口。
2.1.1、认识UserDetailsService
通过上面所说可以知道默认的登录逻辑是不太符合项目开发所需,想要自定义登录逻辑首先认识一个接口 UserDetailsService
2.1.2、认识UserDetails
在上一小节中介绍了 UserDetailsService
这个接口,这个接里有一个 UserDetails loadUserByUsername(String username)
方法;可以知道这个方法他返回了一个UserDetails
,实际上他也是一个接口。他默认的实现类是org.springframework.security.core.userdetails.User
package org.springframework.security.core.userdetails;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import java.io.Serializable;
import java.util.Collection;
/**
* Provides core user information.
*
* <p>
* Implementations are not used directly by Spring Security for security purposes. They
* simply store user information which is later encapsulated into {@link Authentication}
* objects. This allows non-security related user information (such as email addresses,
* telephone numbers etc) to be stored in a convenient location.
* <p>
* Concrete implementations must take particular care to ensure the non-null contract
* detailed for each method is enforced. See
* {@link org.springframework.security.core.userdetails.User} for a reference
* implementation (which you might like to extend or use in your code).
*
* @see UserDetailsService
* @see UserCache
*
* @author Ben Alex
*/
public interface UserDetails extends Serializable {
/**
* 他默认的实现类是org.springframework.security.core.userdetails.User
* User有两个构造器,一个是3参数 一个是7参数
* 当使用 3 个参数的构造器创建对象实例时,实际是还是调用了 7 个参数的构造器。
*/
// ~ Methods
// ========================================================================================================
/**
* Returns the authorities granted to the user. Cannot return <code>null</code>.
*
* @return the authorities, sorted by natural key (never <code>null</code>)
*
* 上面英语的意思是:
* 返回授予用户的权限。不能返回null。
* @返回权限,按自然键排序(绝不为空)
*/
Collection<? extends GrantedAuthority> getAuthorities();// 获取对应的权限 不能返回null
/**
* Returns the password used to authenticate the user.
*
* @return the password
* 上面的英语的意思是:
* 返回用于验证用户身份的密码。
*/
String getPassword();
/**
* Returns the username used to authenticate the user. Cannot return <code>null</code>.
*
* @return the username (never <code>null</code>)
*/
String getUsername();
/**
* Indicates whether the user's account has expired. An expired account cannot be authenticated.
* 用户帐号是否过期。过期的帐户无法进行身份验证。
*
* @return <code>true</code> if the user's account is valid (ie non-expired),
* <code>false</code> if no longer valid (ie expired)
* 如果用户的帐户有效(即未过期)则返回true,如果不再有效(即过期)则返回false
*/
boolean isAccountNonExpired();// 账号是否未过期
/**
* Indicates whether the user is locked or unlocked. A locked user cannot be authenticated.
* 显示用户处于锁定状态或未锁定状态。被锁定的用户无法进行认证。
*
* @return <code>true</code> if the user is not locked, <code>false</code> otherwise
* 如果用户未被锁定,则为True,否则为false
*/
boolean isAccountNonLocked();// 账号是否未被锁定
/**
* Indicates whether the user's credentials (password) has expired. Expired
* credentials prevent authentication.
* 指示用户的凭据(密码)是否已过期。过期凭据阻止身份验证。
*
* @return <code>true</code> if the user's credentials are valid (ie non-expired),
* <code>false</code> if no longer valid (ie expired)
* 如果用户的凭证有效(即未过期),则为True,如果不再有效(即过期),则为false。
*/
boolean isCredentialsNonExpired();//凭证是否未过期 凭证就是密码
/**
* Indicates whether the user is enabled or disabled. A disabled user cannot be authenticated.
* 显示用户是否启用或禁用。禁用的用户无法进行认证。
*
* @return <code>true</code> if the user is enabled, <code>false</code> otherwise
* 如果用户已启用,则为True,否则为false
*/
boolean isEnabled();//账号是否启用 被禁用也是不能登录的
}
UserDetails
他默认的实现类是org.springframework.security.core.userdetails.User
正常的登录逻辑是他会去执行UserDetailsService.loadUserDetailsService
方法,这个方法会使用前端输入的用户名做一些逻辑处理(比如:查数据库或者在当前的应用程序的内存中找匹配的用户名;找到就会返回UserDetails,找不到就抛异常),然后就会得到UserDetails,这个 UserDetails 里面带有他的权限用户名什么的。
2.1.3、认识PasswordEncoder
PasswordEncoder叫密码的解析器,他也是一个接口(java接口)org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
官方推荐使用的实现类。
这个接口对应的实现类是用来实现密码比较验证用的,同时也用于将密码加密成密文。
/*
* Copyright 2011-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.crypto.password;
/**
* Service interface for encoding passwords.
* 编码密码的服务接口。
*
* The preferred implementation is {@code BCryptPasswordEncoder}.
* 首选的实现是{@code BCryptPasswordEncoder}。
*
* @author Keith Donald
* @作者 基思·唐纳德
*/
public interface PasswordEncoder {
/**
* Encode the raw password. Generally, a good encoding algorithm applies a SHA-1 or
* greater hash combined with an 8-byte or greater randomly generated salt.
*/
String encode(CharSequence rawPassword);
// 这个密码就是客户端传入的密码 作用就是加密密码 推荐用SHA-1的算法 或者用哈希 推荐用8位 或者随机的盐加密 这样加密性好一点
// 使用 BCryptPasswordEncoder 实现类进行加密的其中一个特征说一下:这个加密对同样的密码进行多次加密,得到的密文都是不一样的。
// 所以密码的匹配比较也是需要使用 BCryptPasswordEncoder 实现类的 matches(xxx,xx)方法
/**
* Verify the encoded password obtained from storage matches the submitted raw
* password after it too is encoded. Returns true if the passwords match, false if
* they do not. The stored password itself is never decoded.
*
* @param rawPassword the raw password to encode and match
* @param encodedPassword the encoded password from storage to compare with
* @return true if the raw password, after encoding, matches the encoded password from
* storage
*/
boolean matches(CharSequence rawPassword, String encodedPassword);// 参数1:真实的密码;参数2:密码的密文
//客户端传入的密码和我们加密后的密码做比较匹配 对了就返回true
/**
* Returns true if the encoded password should be encoded again for better security,
* else false. The default implementation always returns false.
* @param encodedPassword the encoded password to check
* @return true if the encoded password should be encoded again for better security,
* else false.
*/
default boolean upgradeEncoding(String encodedPassword) {
return false;//加密后的密码再次加密 他默认返回false
}
}
正常的登录逻辑是他会去执行UserDetailsService.loadUserDetailsService
方法,这个方法会使用前端输入的用户名做一些逻辑处理(比如:查数据库或者在当前的应用程序的内存中找匹配的用户名;找到就会返回UserDetails,找不到就抛异常),然后就会得到UserDetails,这个 UserDetails 里面带有他的权限用户名什么的,在Spring Security 框架的内部就会使用 PasswordEncoder 接口对应的实现类来对客户端输入的密码和 UserDetails 里面的密码进行匹配比较,匹配成功就算是登录成功
2.2、实现自定义登录逻辑
通过前面的内容介绍知道了他的登录过程,要实现自定义的登录逻辑也就有了思路。
要想实现自定义的登录逻辑就要写一个UserDetailsService的实现类并重写他的loadUserDetailsService方法然后返回一个UserDetails的实现类就好了。
2.2.1、引入依赖
引入的依赖内容和入门案例使用的依赖是一样的。点击查看
2.2.2、编写代码
-
在
src/main/resources/static
目录下编写一个或者几个html页面(当然不写也没问题); -
在
src/main/java
目录下面创建com.xxx.xxx.controller
包,用来编写Controller代码的,也就是在这个包下创建Controller类,然后编写一个或者几个接口Api。(编写Api时需要注意@Controller和@RestController和@ResponseBody注解对返回结果的影响;后面有介绍,可以往下面看看) -
上面的两个操作至少要操作其中一个;两个步骤都操作一下都行。
-
编写UserDetailService的实现类
package com.xxx.springsecurity.demo.service; import org.springframework.beans.factory.annotation.Autowired; 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.password.PasswordEncoder; import org.springframework.stereotype.Service; @Service public class UserDetailServiceImpl implements UserDetailsService{ //@Autowired //private PasswordEncoder passwordEncoder; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //1、根据用户名去数据库查询 ,如果不存在就抛UsernameNotFoundException。 if(!"admin".equals(username)) {// 为了方便这里就不建数据库表了,也就不查数据库了;就假设数据库中只有 admin 这个用户 throw new UsernameNotFoundException("用户不存在"); } //2、设置一个密码,让 SpringSecurity 内部比较密码 String password = "$2a$10$ODk4SFGfaikJDhHSaxJGg.I0rmPvBUaYcxpivb9HayKtcAzU/EYge";//假设这个是数据库的密码 // 这个密码的原文就是123 也就是使用 passwordEncoder.encode("123"); 得到的密码密文。 //AuthorityUtils 是一个权限工具类,他底层就是通过,号分割的 return new User(username,password,AuthorityUtils.commaSeparatedStringToAuthorityList("权限1,权限2")); // 这里只需要返回一个包含用户名、权限、密文密码的 UserDetails 的实现类就行了。 // 密码的比较校验和权限的检验都由 Spring Security 内部的逻辑来进行 } }
-
编写Spring Security配置类
package com.xxx.springsecurity.demo.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration public class SecurityConfig { // 有一些资料会写这一串代码,在这里不写也没问题。有问题写上也行 // @Autowired // private UserDetailsService userDetailsService; // // @Override // protected void configure(AuthenticationManagerBuilder auth) throws Exception { // // 设置自己定义的 userDetailsService // auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); // } @Bean public PasswordEncoder getPw() { // 自定义登录逻辑的时候容器里面必须要有PasswordEncoder的实例 也就是我们不能直接new PasswordEncoder // 因为 Spring Security 内部会使用 PasswordEncoder 来和 UserDetailsService.loadUserByUsername(username)方法返回的 UserDetails 里面的密码做匹配校验 return new BCryptPasswordEncoder(); } }
2.2.3、测试效果
2.3、自定义登录页面
在实际的项目开发中登录页面一般都是需要特别定制的,Spring Security 内置的登录页面是不能满足实际项目开发所需要的,所以Spring Security 也支持自定义登录页面。实现过程也比较简单,只需要修改配置类就行。
2.3.1、引入依赖
引入的依赖内容和入门案例使用的依赖是一样的。点击查看
2.3.2、编写代码
-
在
src/main/resources/static
目录下编写login.html、main.html、error.html页面。-
login.html
<!DOCTYPE> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <form action="/login" method="post"> 用户名:<input type="text" name="username"> 密码:<input type="password" name="password"> <input type="submit" value="提交"> </form> </body> </html> <!-- 这个是login.html -->
-
main.html
<!DOCTYPE> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div>登录成功</div> </body> </html>
-
error.html
<!DOCTYPE> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div>操作失败,请重新登录<a href="/login.html">跳转</a></div> </body> </html>
-
-
在
src/main/java
目录下面创建com.xxx.springsecurity.demo.controller
包,用来编写Controller代码的,也就是在这个包下创建Controller类,然后编写一个或者几个接口Api。package com.xxx.springsecurity.demo.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; /** * @Controller 和 @RestController 介绍 * * 当你使用 @RestController 注解时,默认情况下所有返回值都会被当作响应体,并且通常会被序列化为JSON或XML格式(取决于你的配置和返回对象类型)。 * * 当你使用 @Controller 注解时,编写的方法返回值是一个字符串时,并且没有使用 @ResponseBody 注解的情况会有下述的情况出现 * 这个字符串可以被视图解析器(如 Thymeleaf, JSP 等)解析为一个视图名称,则会尝试根据这个名称来查找并渲染相应的模板文件(可以理解为页面文件),从而实现页面跳转。 */ @Controller public class TestController { @RequestMapping("/toMain")// 登录成功后通过这个接口Api跳转到 main.html页面。因为不能直接跳转页面 public String toMain() { System.out.println("ssssss"); return "redirect:main.html"; //重定向 /** * @Controller 和 @RestController 介绍 * * 当你使用 @RestController 注解时,默认情况下所有返回值都会被当作响应体,并且通常会被序列化为JSON或XML格式(取决于你的配置和返回对象类型)。 * 就算是字符串也会原封不动的作为响应体内容发送给客户端。 * * 当你使用 @Controller 注解时,编写的方法返回值是一个字符串时,并且没有使用 @ResponseBody 注解的情况会有下述的情况出现 * 这个字符串可以被视图解析器(如 Thymeleaf, JSP 等)解析为一个视图名称,则会尝试根据这个名称来查找并渲染相应的模板文件(可以理解为页面文件),从而实现页面跳转。 * 你可以显式地使用 RedirectView 或者 redirect: 前缀来执行重定向操作,例如返回 "redirect:/someUrl" 会触发302状态码,并告诉浏览器去访问 /someUrl 路径。 * 使用 forward: 前缀可以在服务器端转发请求到另一个URL,而不会改变浏览器地址栏中的URL。例如,返回 "forward:/anotherEndpoint"。 */ } @RequestMapping("/toError") public String toError() { System.out.println("ssssssa"); return "redirect:error.html"; //重定向 } }
-
编写UserDetailService的实现类
package com.xxx.springsecurity.demo.service; import org.springframework.beans.factory.annotation.Autowired; 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.password.PasswordEncoder; import org.springframework.stereotype.Service; @Service public class UserDetailServiceImpl implements UserDetailsService{ //@Autowired //private PasswordEncoder passwordEncoder; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //1、根据用户名去数据库查询 ,如果不存在就抛UsernameNotFoundException。 if(!"admin".equals(username)) {// 为了方便这里就不建数据库表了,也就不查数据库了;就假设数据库中只有 admin 这个用户 throw new UsernameNotFoundException("用户不存在"); } //2、设置一个密码,让 SpringSecurity 内部比较密码 String password = "$2a$10$ODk4SFGfaikJDhHSaxJGg.I0rmPvBUaYcxpivb9HayKtcAzU/EYge";//假设这个是数据库的密码 // 这个密码的原文就是123 也就是使用 passwordEncoder.encode("123"); 得到的密码密文。 //AuthorityUtils 是一个权限工具类,他底层就是通过,号分割的 return new User(username,password,AuthorityUtils.commaSeparatedStringToAuthorityList("权限1,权限2")); // 这里只需要返回一个包含用户名、权限、密文密码的 UserDetails 的实现类就行了。 // 密码的比较校验和权限的检验都由 Spring Security 内部的逻辑来进行 } }
-
编写Spring Security配置类
package com.xxx.springsecurity.demo.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter{//自定义的登录页面需要继承 重写里面的configure(HttpSecurity http) 这个HttpSecurity参数的 @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin()// 调用这个方法表示表单登录的意思,就像入门案例一样,就是一个表单登录。 // 告诉 spring security 自定义的登录页面是哪个,这个位置就是springboot静态资源目录里面的页面,一定要在授权那里放行这个页面 .loginPage("/login.html") // 这个写的是登录页面上提交用户名密码表单的那个action属性值(也就是html中<form>节点的action属性值)(合法的字符可以随便写) // 这个不用在Controller写对应的 Api 接口;只要求和html中<form>节点的action属性值一样就行。 .loginProcessingUrl("/login") // 登录成功后跳转的页面的接口Api,借助这个/toMain 的 Api 接口跳转到 main.html 页面。 // 不能直接写 main.html ,他要求必须是一个post请求才能跳转,直接写 main.html 会以Get请求跳转,会出现错误。所以要借助 Controller 的Api跳转 // 有的版本是 .defaultSuccessUrl("/toMain") 这个方法 .successForwardUrl("/toMain") // 登录失败(错误)后跳转的页面的接口Api,借助这个/toError 的 Api 接口跳转到 error.html 页面。 // 不能直接写 error.html ,他要求必须是一个post请求才能跳转,直接写 error.html 会以Get请求跳转,会出现错误。所以要借助 Controller 的Api跳转 // 记得下面授权放行,不然又被拦截回到登录页面 .failureForwardUrl("/toError"); http.authorizeRequests() .antMatchers("/error.html").permitAll() .antMatchers("/login.html").permitAll()// 登录页面放开权限,用户可以任意访问 //除了放行的请求资源,其他所有请求都必须认证才能访问,必须登录 .anyRequest() .authenticated(); //关闭csrf防护 http.csrf().disable(); } @Bean public PasswordEncoder getPw() { // 自定义登录逻辑的时候容器里面必须要有PasswordEncoder的实例 也就是我们不能直接new PasswordEncoder // 因为 Spring Security 内部会使用 PasswordEncoder 来和 UserDetailsService.loadUserByUsername(username)方法返回的 UserDetails 里面的密码做匹配校验 return new BCryptPasswordEncoder(); } }
-
自定义登录成功处理器和自定义登录失败处理器这里先不介绍。没有自定义的,它也有默认的。
2.3.3、测试效果
2.3.4、自定义登录页面的一些注意事项
1、登录页面的<form>
节点的 methon 属性要写 post
;例如:<form action="/login" method="post">
2、表单中的用户名的key一定要写username;例如:<input type="text" name="username">
3、表单中的密码的key一定要写password;例如:<input type="password" name="password">
4、如果想要改 username 和 password 可以在配置类里进行更改。提交用户名密码一般都是用post的提交方式,这个就算能改建议也不要改。
想要改掉默认的username和password的key可以用下面的配置
package com.xxx.springsecurity.demo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter{//自定义的登录页面需要继承 重写里面的configure(HttpSecurity http) 这个HttpSecurity参数的
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()// 调用这个方法表示表单登录的意思,就像入门案例一样,就是一个表单登录。
.usernameParameter("username123")//自定义登录表单中的用户名参数(用户名的key)
.passwordParameter("password123")//自定义登录表单中的密码参数(密码的key)
// 告诉 spring security 自定义的登录页面是哪个,这个位置就是springboot静态资源目录里面的页面,一定要在授权那里放行这个页面
.loginPage("/login.html")
// 这个写的是登录页面上提交用户名密码表单的那个action属性值(也就是html中<form>节点的action属性值)(合法的字符可以随便写)
// 这个不用在Controller写对应的 Api 接口;只要求和html中<form>节点的action属性值一样就行。
.loginProcessingUrl("/login")
// 登录成功后跳转的页面的接口Api,借助这个/toMain 的 Api 接口跳转到 main.html 页面。
// 不能直接写 main.html ,他要求必须是一个post请求才能跳转,直接写 main.html 会以Get请求跳转,会出现错误。所以要借助 Controller 的Api跳转
// 有的版本是 .defaultSuccessUrl("/toMain") 这个方法
.successForwardUrl("/toMain")
// 登录失败(错误)后跳转的页面的接口Api,借助这个/toError 的 Api 接口跳转到 error.html 页面。
// 不能直接写 error.html ,他要求必须是一个post请求才能跳转,直接写 error.html 会以Get请求跳转,会出现错误。所以要借助 Controller 的Api跳转
// 记得下面授权放行,不然又被拦截回到登录页面
.failureForwardUrl("/toError");
http.authorizeRequests()
.antMatchers("/error.html").permitAll()
.antMatchers("/login.html").permitAll()// 登录页面放开权限,用户可以任意访问
//除了放行的请求资源,其他所有请求都必须认证才能访问,必须登录
.anyRequest()
.authenticated();
//关闭csrf防护
http.csrf().disable();
}
@Bean
public PasswordEncoder getPw() {
// 自定义登录逻辑的时候容器里面必须要有PasswordEncoder的实例 也就是我们不能直接new PasswordEncoder
// 因为 Spring Security 内部会使用 PasswordEncoder 来和 UserDetailsService.loadUserByUsername(username)方法返回的 UserDetails 里面的密码做匹配校验
return new BCryptPasswordEncoder();
}
}
经过上面的配置就可以像下面这样写登录表单了。
<!DOCTYPE>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/login" method="post">
用户名:<input type="text" name="username123"> <!-- 经过上面的配置之后,就可以使用自定义的用户名参数(用户名的key) -->
密码:<input type="password" name="password123"> <!-- 经过上面的配置之后,就可以使用自定义的密码参数(密码的key) -->
<input type="submit" value="提交">
</form>
</body>
</html>
2.3.5、自定义登录成功处理器
前面介绍的认证登录成功是通过配置类中配置的 httpSecurity.successForwardUrl("/toMain")
方法跳转到主页面。查看这个方法的源码就会发现里面就是单纯地做了个请求转发。也就是说被限定死了,如果想在请求转发(页面跳转)前做一些其他的业务逻辑用httpSecurity.successForwardUrl("/toMain")
这个就做不到啦;如果想在登录成功后希望是以重定向的方式去到主页,这时候也是做不到的。
如果想在登录认证成功后想做一些自定义的逻辑和操作这时候就要使用自定义的登录成功处理器了。
引入的依赖内容和入门案例使用的依赖是一样的。点击查看
spring boot启动类
就不多说了,TestController
和UserDetailServiceImpl
的代码参考上面写好的代码示例点击查看。
这里重点介绍一下MyAuthentiticationSuccessHandler
和SecurityConfig
这两个类。
MyAuthentiticationSuccessHandler:
package com.xxx.springsecurity.demo.handler;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
// 这个类就是我们自定义的登录成功处理器。
// 这个类参考着 org.springframework.security.web.authentication.ForwardAuthenticationSuccessHandler 编写的
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private String url;
public MyAuthenticationSuccessHandler(String url) {
this.url = url;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
// TODO Auto-generated method stub
//getCredentials(); 说是获取凭证(密码)
//getDetails() 获取详情
//isAuthenticated 是否认证
//setAuthenticated 设置认证
User user = (User)authentication.getPrincipal();//能获取到登录的用户
System.out.println(user.getUsername());
System.out.println(user.getPassword());//说是位了安全 回输出null
System.out.println(user.getAuthorities());
response.sendRedirect(url);//重定向到url 自己设置自己的登录成功处理逻辑 不用转发
}
}
SecurityConfig:
package com.xxx.springsecurity.demo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter{//自定义的登录页面需要继承 重写里面的configure(HttpSecurity http) 这个HttpSecurity参数的
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()// 调用这个方法表示表单登录的意思,就像入门案例一样,就是一个表单登录。
// 告诉 spring security 自定义的登录页面是哪个,这个位置就是springboot静态资源目录里面的页面,一定要在授权那里放行这个页面
.loginPage("/login.html")
// 这个写的是登录页面上提交用户名密码表单的那个action属性值(也就是html中<form>节点的action属性值)(合法的字符可以随便写)
// 这个不用在Controller写对应的 Api 接口;只要求和html中<form>节点的action属性值一样就行。
.loginProcessingUrl("/login")
// 配置登录成功处理器;这个 MyAuthenticationSuccessHandler 就是上面自定义的那个
.successHandler(new MyAuthenticationSuccessHandler("https://www.baidu.com"))//自定义登录成功处理器
.failureHandler(new MyAuthenticationFailureHandler("https://www.mi.com"));//自定义登录失败处理器 (后面的内容会做出介绍)
http.authorizeRequests()
.antMatchers("/error.html").permitAll()
.antMatchers("/login.html").permitAll()// 登录页面放开权限,用户可以任意访问
//除了放行的请求资源,其他所有请求都必须认证才能访问,必须登录
.anyRequest()
.authenticated();
//关闭csrf防护
http.csrf().disable();
}
@Bean
public PasswordEncoder getPw() {
// 自定义登录逻辑的时候容器里面必须要有PasswordEncoder的实例 也就是我们不能直接new PasswordEncoder
// 因为 Spring Security 内部会使用 PasswordEncoder 来和 UserDetailsService.loadUserByUsername(username)方法返回的 UserDetails 里面的密码做匹配校验
return new BCryptPasswordEncoder();
}
}
2.3.6、自定义登录失败处理器
前面介绍的认证登录失败是通过配置类中配置的 httpSecurity.failureForwardUrl("/toError")
方法跳转到失败相关信息的页面。查看这个方法的源码就会发现里面就是单纯地做了个请求转发。也就是说被限定死了,如果想在请求转发(页面跳转)前做一些其他的业务逻辑用httpSecurity.failureForwardUrl("/toError")
这个就做不到啦;如果想在登录失败后希望是以重定向的方式去到失败页面,这时候也是做不到的。
如果想在登录认证失败后想做一些自定义的逻辑和操作这时候就要使用自定义的登录失败处理器了。
引入的依赖内容和入门案例使用的依赖是一样的。点击查看
spring boot启动类
就不多说了,TestController
和UserDetailServiceImpl
的代码参考上面写好的代码示例点击查看。
这里重点介绍一下MyAuthenticationFailureHandler
和SecurityConfig
这两个类。
MyAuthenticationFailureHandler:
package com.xxx.springsecurity.demo.handler;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
// 这个类就是我们自定义的登录成功处理器。
// 这个类参考着 org.springframework.security.web.authentication.ForwardAuthenticationFailureHandler 编写的
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler{
private String url;
public MyAuthenticationFailureHandler(String url) {
this.url = url;
}
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
//AuthenticationException 就是一个异常,认证失败的那个异常信息。
response.sendRedirect(url);// 这里做个重定向模拟自定义登录失败的逻辑。
}
}
SecurityConfig:
package com.xxx.springsecurity.demo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter{//自定义的登录页面需要继承 重写里面的configure(HttpSecurity http) 这个HttpSecurity参数的
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()// 调用这个方法表示表单登录的意思,就像入门案例一样,就是一个表单登录。
// 告诉 spring security 自定义的登录页面是哪个,这个位置就是springboot静态资源目录里面的页面,一定要在授权那里放行这个页面
.loginPage("/login.html")
// 这个写的是登录页面上提交用户名密码表单的那个action属性值(也就是html中<form>节点的action属性值)(合法的字符可以随便写)
// 这个不用在Controller写对应的 Api 接口;只要求和html中<form>节点的action属性值一样就行。
.loginProcessingUrl("/login")
.successHandler(new MyAuthenticationSuccessHandler("https://www.baidu.com"))//自定义登录成功处理器(前面的内容已经介绍过了)
// 配置登录失败处理器;这个 MyAuthenticationFailureHandler 就是上面自定义的那个
.failureHandler(new MyAuthenticationFailureHandler("https://www.mi.com"));//自定义登录失败处理器
http.authorizeRequests()
.antMatchers("/error.html").permitAll()
.antMatchers("/login.html").permitAll()// 登录页面放开权限,用户可以任意访问
//除了放行的请求资源,其他所有请求都必须认证才能访问,必须登录
.anyRequest()
.authenticated();
//关闭csrf防护
http.csrf().disable();
}
@Bean
public PasswordEncoder getPw() {
// 自定义登录逻辑的时候容器里面必须要有PasswordEncoder的实例 也就是我们不能直接new PasswordEncoder
// 因为 Spring Security 内部会使用 PasswordEncoder 来和 UserDetailsService.loadUserByUsername(username)方法返回的 UserDetails 里面的密码做匹配校验
return new BCryptPasswordEncoder();
}
}
三、授权
Spring Security 的授权是这个框架的核心功能之一,主要用于控制用户对特定资源或功能的访问权限。以下是详细介绍:
3.1、匹配请求和授权放行请求
在实际项目开发中,会有一些不需要用户登录就能访问的请求。针对这类请求 Spring Security 内置有专门的方法(函数),用于对这些无需用户登录认证的请求进行匹配及授权放行的操作,我们只需将相应的代码准确编写出来,即可达成对这些无需登录请求的合理放行。
3.1.1、anyRequest()方法
这个方法从名字上看可以知道他是任何请求(所有请求)的意思。用法如下所示:
3.1.2、antMatchers()方法(常用)
从方法名上看,他有匹配的意思;可以理解为匹配某些路径、匹配某些请求的意思。
它有3个重载的方法:
antMatchers(String... antPatterns)
:方法参数是一个可变参数类型,可以写一个或者多个字符串,用来匹配某些路径、匹配某些请求的意思。antMatchers(HttpMethod method, String... antPatterns)
:方法参数是一个可变参数类型,可以写一个或者多个字符串,用来匹配某些路径、匹配某些请求的意思;同时还限定了 Http 请求的 Method (Get、Post、Put等)。antMatchers(HttpMethod method)
:用来匹配 Http 请求的 Method 意思(Get、Post、Put等)。
用法如下所示:
3.1.3、regexMatchers()方法
从方法名上看,他有正则表达式匹配的意思;可以理解为匹配某些路径、匹配某些请求的意思。用法如下所示:
.anyRequest().authenticated();
这句话要放在最后面,放在antMatchers()
、 regexMatchers()
前面启动项目时会报错。
因为能匹配请求规则的放行,其他的任何请求都要登录认证,这样的顺序才是合乎正常逻辑的。
如果是.anyRequest().authenticated()
放在前面,就是任何请求都要登录认证,然后是匹配规则的放行,在人类的世界能理解,但是在应用程序中是不好理解的
3.1.4、mvcMatchers()方法
从方法名上看,他可以理解为MVC匹配的意思;也是用于匹配某些路径、匹配某些请求的意思。(一般不用这个)
用法如下所示:
3.1.5、permitAll()方法(授权放行)
从前面的介绍可以看到 antMatchers()
或者 regexMatchers()
等匹配方法后面都带有这个permitAll()
,有了这个 permitAll()
方法才能对匹配的资源可以进行任意访问。
下面就来看一下这个方法的内容:
3.2、基于权限的授权
在Spring Security中登录认证成功后,还可以通过设置权限来控制登录的用户哪些资源(API)可以访问,哪些是不能能访问的。
这个章节介绍的权限授权,其实关键的是配置类中的授权部分的 .hasAnyAuthority("权限1","权限3")
方法
3.2.1、编写代码
代码结构和自定义登录页面是一样的,代码的内容和自定义登录页面也是几乎一样的;代码就先按照自定义登录页面的代码写一份(点击这里查看自定义登录页面的代码)。不同的地方是 main.html 页面、Spring Security配置类、另外还多了一个 authorityTest.html 页面。
main.html页面的改动如下所示:
<!DOCTYPE>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div>登录成功</div>
<!--
其实和上面介绍的自定义登录页面的代码逻辑就多了这一行
这一行的代码是为了让登录成功后直接点击这里跳转一个页面来进行测试授权权限是否起效果。
-->
<div><a href="authorityTest.html">跳转-authorityTest</a></div>
</body>
</html>
在 /src/main/resources/static
目录下(也就是在main.html页面同一个目录下)创建一个 authorityTest.html
<!DOCTYPE>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div>AuthorityTest.权限控制-测试</div>
</body>
</html>
Spring Security配置类
package com.xxx.springsecurity.demo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter{//自定义的登录页面需要继承 重写里面的configure(HttpSecurity http) 这个HttpSecurity参数的
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()// 调用这个方法表示表单登录的意思,就像入门案例一样,就是一个表单登录。
// 告诉 spring security 自定义的登录页面是哪个,这个位置就是springboot静态资源目录里面的页面,一定要在授权那里放行这个页面
.loginPage("/login.html")
// 这个写的是登录页面上提交用户名密码表单的那个action属性值(也就是html中<form>节点的action属性值)(合法的字符可以随便写)
// 这个不用在Controller写对应的 Api 接口;只要求和html中<form>节点的action属性值一样就行。
.loginProcessingUrl("/login")
// 登录成功后跳转的页面的接口Api,借助这个/toMain 的 Api 接口跳转到 main.html 页面。
// 不能直接写 main.html ,他要求必须是一个post请求才能跳转,直接写 main.html 会以Get请求跳转,会出现错误。所以要借助 Controller 的Api跳转
// 有的版本是 .defaultSuccessUrl("/toMain") 这个方法
.successForwardUrl("/toMain")
// 登录失败(错误)后跳转的页面的接口Api,借助这个/toError 的 Api 接口跳转到 error.html 页面。
// 不能直接写 error.html ,他要求必须是一个post请求才能跳转,直接写 error.html 会以Get请求跳转,会出现错误。所以要借助 Controller 的Api跳转
// 记得下面授权放行,不然又被拦截回到登录页面
.failureForwardUrl("/toError");
http.authorizeRequests()
.antMatchers("/error.html").permitAll()
.antMatchers("/login.html").permitAll()// 登录页面放开权限,用户可以任意访问
.antMatchers("/js/**","/css/**","/images/**").permitAll()//.permitAll()这个后就是放行这些静态资源
//权限控制,严格区分大小写。登录成功的时候用户所带有的权限。有这个权限就能访问这个资源
//.antMatchers("/authorityTest.html").hasAuthority("权限1")
//也是权限控制的一种,严格区分大小写。登录成功的时候用户所带有的权限。 如果这个人没有权限3.但是却有权限1还是能访问这个资源的
.antMatchers("/authorityTest.html").hasAnyAuthority("权限1","权限3")
//除了放行的请求资源,其他所有请求都必须认证才能访问,必须登录
.anyRequest()
.authenticated();
//关闭csrf防护
http.csrf().disable();
}
@Bean
public PasswordEncoder getPw() {
// 自定义登录逻辑的时候容器里面必须要有PasswordEncoder的实例 也就是我们不能直接new PasswordEncoder
// 因为 Spring Security 内部会使用 PasswordEncoder 来和 UserDetailsService.loadUserByUsername(username)方法返回的 UserDetails 里面的密码做匹配校验
return new BCryptPasswordEncoder();
}
}
3.2.2、测试
3.2.3、解释说明
userDetailService中的那个权限列表在起作用
3.3、基于角色的授权
在Spring Security中登录认证成功后,还可以通过设置角色来控制登录的用户哪些资源(API)可以访问,哪些是不能能访问的。
这个章节介绍的角色授权,其实关键的是配置类中的授权部分的 .hasAnyRole("角色1","角色5")
方法
3.3.1、编写代码
代码结构和自定义登录页面是一样的,代码的内容和自定义登录页面也是几乎一样的;代码就先按照自定义登录页面的代码写一份(点击这里查看自定义登录页面的代码)。不同的地方是 main.html页面、Spring Security配置类、另外还多了一个 roleTest.html 页面、另外还需要在UserDetailsService实现类添加上角色。
main.html页面的改动如下所示:
<!DOCTYPE>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div>登录成功</div>
<!--
其实和上面介绍的自定义登录页面的代码逻辑就多了这一行
这一行的代码是为了让登录成功后直接点击这里跳转一个页面来进行测试角色权限是否起效果。
-->
<div><a href="roleTest.html">跳转-RoleTest</a></div>
</body>
</html>
在 /src/main/resources/static
目录下(也就是在main.html页面同一个目录下)创建一个 roleTest.html
<!DOCTYPE>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div>RoleTest.角色权限控制-测试</div>
</body>
</html>
在UserDetailsService类中添加角色。
package com.xxx.springsecurity.demo.service;
import org.springframework.beans.factory.annotation.Autowired;
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.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@Service
public class UserDetailServiceImpl implements UserDetailsService{
//@Autowired
//private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//1、根据用户名去数据库查询 ,如果不存在就抛UsernameNotFoundException。
if(!"admin".equals(username)) {// 为了方便这里就不建数据库表了,也就不查数据库了;就假设数据库中只有 admin 这个用户
throw new UsernameNotFoundException("用户不存在");
}
//2、设置一个密码,让 SpringSecurity 内部比较密码
String password = "$2a$10$ODk4SFGfaikJDhHSaxJGg.I0rmPvBUaYcxpivb9HayKtcAzU/EYge";//假设这个是数据库的密码
// 这个密码的原文就是123 也就是使用 passwordEncoder.encode("123"); 得到的密码密文。
// 基于角色的授权需要在角色名前面加上ROLE_;例如:ROLE_角色1;
// 基于角色的授权 和 基于权限的授权是可以写在一起的
// AuthorityUtils 是一个权限工具类,他底层就是通过,号分割的。
return new User(username,password,AuthorityUtils.commaSeparatedStringToAuthorityList("权限1,权限2,ROLE_角色1,ROLE_角色2"));
// 这里只需要返回一个包含用户名、权限、密文密码的 UserDetails 的实现类就行了。
// 密码的比较校验和权限的检验都由 Spring Security 内部的逻辑来进行
}
}
Spring Security配置类
package com.xxx.springsecurity.demo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter{//自定义的登录页面需要继承 重写里面的configure(HttpSecurity http) 这个HttpSecurity参数的
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()// 调用这个方法表示表单登录的意思,就像入门案例一样,就是一个表单登录。
// 告诉 spring security 自定义的登录页面是哪个,这个位置就是springboot静态资源目录里面的页面,一定要在授权那里放行这个页面
.loginPage("/login.html")
// 这个写的是登录页面上提交用户名密码表单的那个action属性值(也就是html中<form>节点的action属性值)(合法的字符可以随便写)
// 这个不用在Controller写对应的 Api 接口;只要求和html中<form>节点的action属性值一样就行。
.loginProcessingUrl("/login")
// 登录成功后跳转的页面的接口Api,借助这个/toMain 的 Api 接口跳转到 main.html 页面。
// 不能直接写 main.html ,他要求必须是一个post请求才能跳转,直接写 main.html 会以Get请求跳转,会出现错误。所以要借助 Controller 的Api跳转
// 有的版本是 .defaultSuccessUrl("/toMain") 这个方法
.successForwardUrl("/toMain")
// 登录失败(错误)后跳转的页面的接口Api,借助这个/toError 的 Api 接口跳转到 error.html 页面。
// 不能直接写 error.html ,他要求必须是一个post请求才能跳转,直接写 error.html 会以Get请求跳转,会出现错误。所以要借助 Controller 的Api跳转
// 记得下面授权放行,不然又被拦截回到登录页面
.failureForwardUrl("/toError");
http.authorizeRequests()
.antMatchers("/error.html").permitAll()
.antMatchers("/login.html").permitAll()// 登录页面放开权限,用户可以任意访问
.antMatchers("/js/**","/css/**","/images/**").permitAll()//.permitAll()这个后就是放行这些静态资源
//角色权限控制,严格区分大小写。登录成功的时候用户所带有的权限(角色)。有这个权限就能访问这个资源
//.antMatchers("/roleTest.html").hasRole("角色1")
//也是角色权限控制的一种,严格区分大小写。登录成功的时候用户所带有的权限(角色)。 如果这个人没有角色5.但是却有角色1还是能访问这个资源的
.antMatchers("/roleTest.html").hasAnyRole("角色1","角色5")
//除了放行的请求资源,其他所有请求都必须认证才能访问,必须登录
.anyRequest()
.authenticated();
//关闭csrf防护
http.csrf().disable();
}
@Bean
public PasswordEncoder getPw() {
// 自定义登录逻辑的时候容器里面必须要有PasswordEncoder的实例 也就是我们不能直接new PasswordEncoder
// 因为 Spring Security 内部会使用 PasswordEncoder 来和 UserDetailsService.loadUserByUsername(username)方法返回的 UserDetails 里面的密码做匹配校验
return new BCryptPasswordEncoder();
}
}
3.3.2、测试
3.3.3、解释说明
userDetailService中的那个权限列表在起作用
3.4、基于IP的授权(不常用)
在 spring security 中还有可以基于 IP 的授权方式,不过这个方式不常用,了解一下就好。其实关键的就是在配置类中的授权配置部分的 .antMatchers("/main1.html").hasIpAddress("127.0.0.1")
方法。
先按照基于权限的授权的内容把代码先写出来;关键的地方是改改配置类就好。修改的地方如下所示:
测试也是可以按照基于权限的授权的测试步骤来进行。
3.5、基于access方法的授权控制
在前面介绍permitAll()方法的时候有初步的介绍过access()
方法,翻到上面的章节来看同时结合着permitAll()
方法的源码来看,实际上permitAll()
方法就是调用access(xxx)
的方法;基于permitAll()
方法的这种情况,尝试着翻开hasAuthority()
,hasAnyAuthority()
基于权限授权的方法和hasRole()
,hasAnyRole()
基于角色授权的方法的源码,就会发现这些基于权限或者基于角色的授权方法实际上都是调用了access(xxx)方法,只是参数不一样而已。
这么来看access(xxx)
方法是可以做到权限授权的控制的,在配置类中我们也是可以使用access(xxx)
方法来进行授权配置的。
先按照基于权限的授权的内容把代码先写出来;关键的地方是改改配置类就好。修改的地方如下所示;
Spring Security 提供了一系列基于表达式的访问控制方法,这些表达式基于 Spring 表达式语言(SpEL),这些表达式在Spring Security中有些资料中又称为access表达式;用于在配置中定义细粒度的权限控制逻辑。以下是一些常用的表达式及其用途:
一些Spring Security 基于表达式的访问控制的示例:
package com.xxx.springsecurity.demo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter{//自定义的登录页面需要继承 重写里面的configure(HttpSecurity http) 这个HttpSecurity参数的
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()// 调用这个方法表示表单登录的意思,就像入门案例一样,就是一个表单登录。
// 告诉 spring security 自定义的登录页面是哪个,这个位置就是springboot静态资源目录里面的页面,一定要在授权那里放行这个页面
.loginPage("/login.html")
// 这个写的是登录页面上提交用户名密码表单的那个action属性值(也就是html中<form>节点的action属性值)(合法的字符可以随便写)
// 这个不用在Controller写对应的 Api 接口;只要求和html中<form>节点的action属性值一样就行。
.loginProcessingUrl("/login")
// 登录成功后跳转的页面的接口Api,借助这个/toMain 的 Api 接口跳转到 main.html 页面。
// 不能直接写 main.html ,他要求必须是一个post请求才能跳转,直接写 main.html 会以Get请求跳转,会出现错误。所以要借助 Controller 的Api跳转
// 有的版本是 .defaultSuccessUrl("/toMain") 这个方法
.successForwardUrl("/toMain")
// 登录失败(错误)后跳转的页面的接口Api,借助这个/toError 的 Api 接口跳转到 error.html 页面。
// 不能直接写 error.html ,他要求必须是一个post请求才能跳转,直接写 error.html 会以Get请求跳转,会出现错误。所以要借助 Controller 的Api跳转
// 记得下面授权放行,不然又被拦截回到登录页面
.failureForwardUrl("/toError");
//表达式的使用示例如下:
http.authorizeRequests()
/**
* 前面有介绍过在 Spring Security 中,access() 方法的参数是一个 SpEL(Spring Expression Language)表达式
* 表达式的介绍如下(表达式基本语法):
* 一、直接调用内置的表达式:例如 .access("hasRole('角色1')")。
* 二、表达式支持逻辑运算符,支持 and、or、! not(非)。例如 .access("hasRole('角色1') or hasAuthority('权限2')")。
* 三、表达式中可以写一些 Spring Security 内置的引用对象;例如:例如 authentication(当前认证信息)、request(HTTP 请求对象)、principal: 用户主体(通常是 UserDetails 对象)。
* 四、表达式中可以写自定义 Bean:例如:@beanName.method() ,它就会调用 Spring 容器中的 Bean;这时候可以在这个 Bean 中自定义权限检查逻辑
*/
// 1、允许所有用户访问(包括未登录)--> .access("permitAll")
.antMatchers("/error.html").access("permitAll")
.antMatchers("/login.html").access("permitAll")// 登录页面放开权限,用户可以任意访问
.antMatchers("/js/**","/css/**","/images/**").permitAll()//.permitAll()这个后就是放行这些静态资源
// 2、仅允许认证用户访问(登录认证后的用户才能访问) --> .access("isAuthenticated()")
.antMatchers("/xxx").access("isAuthenticated()")
// 3、必须未登录(用于登录页面)匿名访问嘛
.antMatchers("/xxx").access("isAnonymous()")
// 4、基于角色的访问控制 --> .access("hasRole('角色1')"),hasRole 方法会自动在角色名称前加上 ROLE_ 前缀,因此你只需要传入角色名称即可
.antMatchers("/xxx").access("hasRole('角色1')")
.antMatchers("/xxx").access("hasAnyRole('角色1','角色2')")
// 5、基于权限的访问控制 --> .access("hasAuthority('权限1')")
.antMatchers("/xxx").access("hasAuthority('权限1')")
.antMatchers("/xxx").access("hasAnyAuthority('权限1','权限3')")
// 6、要求请求来自特定 IP 地址
.antMatchers("/xxx").access("hasIpAddress('192.168.1.0/24')")
// 7、基于组合条件的访问控制。(支持逻辑运算符,支持 and、or、!(非))。
.antMatchers("/xxx").access("hasRole('角色1') or hasAuthority('权限2')")//这表示具有 '角色1' 角色 或 '权限2' 权限的用户都可以访问 /xxx。
.antMatchers("/xxx").access("hasRole('角色1') and hasIpAddress('192.168.1.1')") //这表示同时需要 '角色1' 角色和特定IP的用户才可以访问 /xxx。
.antMatchers("/user/**").access("hasAuthority('user:add') and !hasAuthority('user:delete')")//在这个例子中,只有同时拥有 user:add 权限且没有 user:delete 权限的用户才能访问 /user/** 路径
// 8、基于用户属性的访问控制。
// 表达式中可以写一些 Spring Security 内置的引用对象;例如:例如 authentication(当前认证信息)、principal: 用户主体(通常是 UserDetails 对象)、request(HTTP 请求对象)。
.antMatchers("/xxx").access("principal.username == 'admin'") //要求UserDetails中用户名为 admin 的用户才能访问。
.antMatchers("/xxx").access("principal.email.endsWith('@company.com')")// 要求用户的邮箱域名为 @company.com
.antMatchers("/xxx").access("principal.authorities.contains('ROLE_ADMIN')")//principal.authorities 是一个集合,表示当前用户的所有权限。此表达式表示只有具有 ROLE_ADMIN 权限的用户才能访问
.antMatchers("/xxx").access("authentication.name == 'admin'")// 要求用户的 username 是 'admin'
.antMatchers("/xxx").access("authentication.details.remoteAddress == '127.0.0.1'")
.antMatchers("/user/{id}").access("principal.id == #id")
.antMatchers("/user/{id}").access("isAuthenticated() and principal.id == #id") // 需要认证且用户 ID 与路径参数匹配
// 9、基于请求的访问控制。 表达式中可以写一些 Spring Security 内置的引用对象;所以写 request 是可以的
.antMatchers("/xxx").access("request.method == 'GET'")//要求请求方法为 GET
.antMatchers("/xxx").access("request.getParameter('debug') == 'true'")// 检查请求参数(例如必须有 ?debug=true)
// 10、基于时间或环境变量
.antMatchers("/xxx").access("T(java.time.LocalTime).now().isBetween('09:00', '18:00')")// 仅在 9:00-18:00 允许访问
.antMatchers("/xxx").access("T(System).getenv('APP_MODE') == 'prod'")// 根据系统环境变量控制访问
/**
* 11、调用自定义权限检查逻辑;这里假设Spring容器中有一个名称为 myAccessService 的 Bean,其中包含方法 checkAccess()
* 这个 Bean 当然是要自己编写,自定义权限检查逻辑就自己编写在 hasPermission(Authentication, Request) 方法中;下面会有详细介绍
*/
.antMatchers("/xxx").access("@myAccessService.hasPermission(authentication, request)")
//除了放行的请求资源,其他所有请求都必须认证才能访问,必须登录
.anyRequest().authenticated();
//关闭csrf防护
http.csrf().disable();
}
@Bean
public PasswordEncoder getPw() {
// 自定义登录逻辑的时候容器里面必须要有PasswordEncoder的实例 也就是我们不能直接new PasswordEncoder
// 因为 Spring Security 内部会使用 PasswordEncoder 来和 UserDetailsService.loadUserByUsername(username)方法返回的 UserDetails 里面的密码做匹配校验
return new BCryptPasswordEncoder();
}
}
也可以去百度搜索一下Spring Security 基于表达式的访问控制。
上面介绍的授权示例已经能满足大多数的权限授权情况了,如果说授权的表达式还是不能满足项目的开发需求,就得要按照示例中的第11点来编写一个自行定制更复杂的授权逻辑。
操作如下:
-
先按照基于权限的授权的内容把代码先写出来
-
创建
com.xxx.springsecurity.demo.access
包,在这个包中创建一个 interface (java接口) 接口的名称就叫MyAccess
;在接口中声明一个方法:public boolean hasPermission(Authentication authentication, HttpServletRequest req);// 其中 Authentication 参数是用来获取UserDetails
;不用这个接口应该也是可以的。 -
创建
com.xxx.springsecurity.demo.access.impl
包,在这个包中创建一个java类,类名就叫MyAccessService
,这个类实现MyAccess
接口,然后实现MyAccess
接口中的方法。package com.xxx.springsecurity.demo.access.impl; import java.util.Collection; import javax.servlet.http.HttpServletRequest; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Component; import com.xxx.springsecurity.demo.access.MyAccess; @Component//一定要加这个注解,让这个类成为Spring容器中的一个组件; public class MyAccessService implements MyAccess{ @Override public boolean hasPermission(Authentication authentication, HttpServletRequest req) {//Authentication 用来获取UserDetails //1、获取主体 Object obj = authentication.getPrincipal(); //2、判断主体是否属于 UserDetails if(obj instanceof UserDetails) { UserDetails user = (UserDetails)obj; //3、获取用户的权限列表 Collection<? extends GrantedAuthority> authorities = user.getAuthorities(); return authorities.contains(new SimpleGrantedAuthority(req.getRequestURI()));//可以借用这个类 SimpleGrantedAuthority来判断是否拥有对应的uri } return false; } //这只是个示例,其实可以多写几个方法,用于不同的授权检查。 }
-
编写配置类中的配置代码,在配置类中使用 accesss 表达式来进行对资源的访问控制。
package com.xxx.springsecurity.demo.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter{//自定义的登录页面需要继承 重写里面的configure(HttpSecurity http) 这个HttpSecurity参数的 @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin()// 调用这个方法表示表单登录的意思,就像入门案例一样,就是一个表单登录。 // 告诉 spring security 自定义的登录页面是哪个,这个位置就是springboot静态资源目录里面的页面,一定要在授权那里放行这个页面 .loginPage("/login.html") // 这个写的是登录页面上提交用户名密码表单的那个action属性值(也就是html中<form>节点的action属性值)(合法的字符可以随便写) // 这个不用在Controller写对应的 Api 接口;只要求和html中<form>节点的action属性值一样就行。 .loginProcessingUrl("/login") // 登录成功后跳转的页面的接口Api,借助这个/toMain 的 Api 接口跳转到 main.html 页面。 // 不能直接写 main.html ,他要求必须是一个post请求才能跳转,直接写 main.html 会以Get请求跳转,会出现错误。所以要借助 Controller 的Api跳转 // 有的版本是 .defaultSuccessUrl("/toMain") 这个方法 .successForwardUrl("/toMain") // 登录失败(错误)后跳转的页面的接口Api,借助这个/toError 的 Api 接口跳转到 error.html 页面。 // 不能直接写 error.html ,他要求必须是一个post请求才能跳转,直接写 error.html 会以Get请求跳转,会出现错误。所以要借助 Controller 的Api跳转 // 记得下面授权放行,不然又被拦截回到登录页面 .failureForwardUrl("/toError"); http.authorizeRequests() .antMatchers("/error.html").permitAll() .antMatchers("/login.html").permitAll()// 登录页面放开权限,用户可以任意访问 .antMatchers("/js/**","/css/**","/images/**").permitAll()//.permitAll()这个后就是放行这些静态资源 //权限控制,严格区分大小写。登录成功的时候用户所带有的权限。有这个权限就能访问这个资源 //.antMatchers("/authorityTest.html").hasAuthority("权限1") //也是权限控制的一种,严格区分大小写。登录成功的时候用户所带有的权限。 如果这个人没有权限3.但是却有权限1还是能访问这个资源的 .antMatchers("/authorityTest.html").hasAnyAuthority("权限1","权限3") // 任何请求都要经过MyAccessService类中的hasPermission方法进行权限检查。 .anyRequest().access("@myAccessService.hasPermission(authentication, request)"); //关闭csrf防护 http.csrf().disable(); } @Bean public PasswordEncoder getPw() { // 自定义登录逻辑的时候容器里面必须要有PasswordEncoder的实例 也就是我们不能直接new PasswordEncoder // 因为 Spring Security 内部会使用 PasswordEncoder 来和 UserDetailsService.loadUserByUsername(username)方法返回的 UserDetails 里面的密码做匹配校验 return new BCryptPasswordEncoder(); } }
3.6、基于@Secured注解的访问控制
在 Spring Security 中提供了一些访问控制的注解。下面介绍一下基于@Secured
注解的使用。
@Secured
注解一般常用于角色权限的访问控制。能写在类上或者方法上;参数按照规范来说要以 ROLE_
开头。一般来说这个注解应该写在Controller的方法上,写在service上会出现权限不好控制的情况,因为不同的Controller的方法可能会调用到同一个Service的同一个方法。
使用@Secured注解进行资源的访问控制的操作如下:
-
先按照基于角色的授权的内容把代码先写出来;
-
在启动类或者Spring Security配置类上(二者选其一就行)使用
@EnableGlobalMethodSecurity(securedEnabled = true)
注解,关键是将 securedEnabled 属性设为true,这样做是将基于@Secured注解进行资源的访问控制开启起来。 -
修改Spring Security配置类,将这个配置类中基于角色进行资源访问控制的代码给注释掉或者删除掉;因为现在已经在使用基于
@Secured
注解进行资源的访问的控制了,而@Secured
注解一般常用于角色权限的访问控制,所以就不要使用了基于@Secured注
解的角色权限访问控制,又在配置类里又配置角色权限控制了。package com.xxx.springsecurity.demo.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration @EnableGlobalMethodSecurity(securedEnabled = true)// 将基于@Secured注解进行资源的访问控制开启起来。 关键是将 securedEnabled 设为 true public class SecurityConfig extends WebSecurityConfigurerAdapter{//自定义的登录页面需要继承 重写里面的configure(HttpSecurity http) 这个HttpSecurity参数的 @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin()// 调用这个方法表示表单登录的意思,就像入门案例一样,就是一个表单登录。 // 告诉 spring security 自定义的登录页面是哪个,这个位置就是springboot静态资源目录里面的页面,一定要在授权那里放行这个页面 .loginPage("/login.html") // 这个写的是登录页面上提交用户名密码表单的那个action属性值(也就是html中<form>节点的action属性值)(合法的字符可以随便写) // 这个不用在Controller写对应的 Api 接口;只要求和html中<form>节点的action属性值一样就行。 .loginProcessingUrl("/login") // 登录成功后跳转的页面的接口Api,借助这个/toMain 的 Api 接口跳转到 main.html 页面。 // 不能直接写 main.html ,他要求必须是一个post请求才能跳转,直接写 main.html 会以Get请求跳转,会出现错误。所以要借助 Controller 的Api跳转 // 有的版本是 .defaultSuccessUrl("/toMain") 这个方法 .successForwardUrl("/toMain") // 登录失败(错误)后跳转的页面的接口Api,借助这个/toError 的 Api 接口跳转到 error.html 页面。 // 不能直接写 error.html ,他要求必须是一个post请求才能跳转,直接写 error.html 会以Get请求跳转,会出现错误。所以要借助 Controller 的Api跳转 // 记得下面授权放行,不然又被拦截回到登录页面 .failureForwardUrl("/toError"); http.authorizeRequests() .antMatchers("/error.html").permitAll() .antMatchers("/login.html").permitAll()// 登录页面放开权限,用户可以任意访问 .antMatchers("/js/**","/css/**","/images/**").permitAll()//.permitAll()这个后就是放行这些静态资源 //使用了@Secured注解进行对不同的角色对资源的访问控制,就不要在配置类里配置角色授权了,建议统一都用@Secured注解 //.antMatchers("/roleTest.html").hasRole("角色1") //使用了@Secured注解进行对不同的角色对资源的访问控制,就不要在配置类里配置角色授权了,建议统一都用@Secured注解 //.antMatchers("/roleTest.html").hasAnyRole("角色1","角色5") //除了放行的请求资源,其他所有请求都必须认证才能访问,必须登录 .anyRequest().authenticated(); //关闭csrf防护 http.csrf().disable(); } @Bean public PasswordEncoder getPw() { // 自定义登录逻辑的时候容器里面必须要有PasswordEncoder的实例 也就是我们不能直接new PasswordEncoder // 因为 Spring Security 内部会使用 PasswordEncoder 来和 UserDetailsService.loadUserByUsername(username)方法返回的 UserDetails 里面的密码做匹配校验 return new BCryptPasswordEncoder(); } }
-
在Controller中的方法上使用@Secured注解;注解的属性值通常以
ROLE_
开头,表示用户所属的群体或角色。例如,ROLE_USER
、ROLE_ADMIN
。package com.xxx.springsecurity.demo.controller; import org.springframework.security.access.annotation.Secured; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; /** * @Controller 和 @RestController 介绍 * * 当你使用 @RestController 注解时,默认情况下所有返回值都会被当作响应体,并且通常会被序列化为JSON或XML格式(取决于你的配置和返回对象类型)。 * * 当你使用 @Controller 注解时,编写的方法返回值是一个字符串时,并且没有使用 @ResponseBody 注解的情况会有下述的情况出现 * 这个字符串可以被视图解析器(如 Thymeleaf, JSP 等)解析为一个视图名称,则会尝试根据这个名称来查找并渲染相应的模板文件(可以理解为页面文件),从而实现页面跳转。 */ @Controller public class TestController { @Secured("ROLE_角色5") //注解的属性值通常以 ROLE_ 开头,表示用户所属的群体或角色。 测试的时候调整这里的权限就好 @RequestMapping("/toMain")// 登录成功后通过这个接口Api跳转到 main.html页面。因为不能直接跳转页面 public String toMain() { System.out.println("ssssss"); return "redirect:main.html"; //重定向 /** * @Controller 和 @RestController 介绍 * * 当你使用 @RestController 注解时,默认情况下所有返回值都会被当作响应体,并且通常会被序列化为JSON或XML格式(取决于你的配置和返回对象类型)。 * 就算是字符串也会原封不动的作为响应体内容发送给客户端。 * * 当你使用 @Controller 注解时,编写的方法返回值是一个字符串时,并且没有使用 @ResponseBody 注解的情况会有下述的情况出现 * 这个字符串可以被视图解析器(如 Thymeleaf, JSP 等)解析为一个视图名称,则会尝试根据这个名称来查找并渲染相应的模板文件(可以理解为页面文件),从而实现页面跳转。 * 你可以显式地使用 RedirectView 或者 redirect: 前缀来执行重定向操作,例如返回 "redirect:/someUrl" 会触发302状态码,并告诉浏览器去访问 /someUrl 路径。 * 使用 forward: 前缀可以在服务器端转发请求到另一个URL,而不会改变浏览器地址栏中的URL。例如,返回 "forward:/anotherEndpoint"。 */ } @RequestMapping("/toError") public String toError() { System.out.println("ssssssa"); return "redirect:error.html"; //重定向 } }
-
测试:如果登录的用户拥有对应的角色权限就能正常的访问Api资源;如果登录的用户没有对应的角色权限将会报
org.springframework.security.access.AccessDeniedException: 不允许访问
异常;就算使用了自定义的403处理器(自定义的访问拒绝处理器)也一样会报异常。
3.7、基于@PreAuthorize注解的访问控制
@PreAuthorize
是 Spring Security 提供的一个非常强大的注解,用于在方法执行之前进行权限检查。它是基于表达式的访问控制,允许开发者使用复杂的逻辑来决定是否允许某个方法的调用,比如:根据用户角色、权限、方法参数甚至业务逻辑动态判断访问方法的权限。与 @Secured
注解相比,@PreAuthorize
提供了更高的灵活性,支持更复杂的表达式和条件判断。
使用@PreAuthorize
注解和使用@Secured
注解差不多,但是关键的就是注解的属性值,@PreAuthorize
注解的属性值写的是Spring表达式(又叫SpEL表达式,通俗叫access表达式)。因为这个注解就是基于表达式的访问控制,access表达式可以查看基于access方法的授权控制章节。
-
先按照基于权限的授权的内容把代码先写出来;
-
在启动类或者Spring Security配置类上(二者选其一就行)使用
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled=true)
注解,关键是将 prePostEnabled 属性设为true,这样做是将基于@PreAuthorize
注解进行资源的访问控制开启起来。 -
修改Spring Security配置类,将这个配置类中控制资源访问的代码给注释掉或者删除掉;因为现在已经在使用基于
@PreAuthorize
注解进行资源的访问的控制了,所以就不要使用了基于@PreAuthorize
注解的时候,又在配置类里又配置资源的权限控制了。package com.xxx.springsecurity.demo.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration // 将基于@PreAuthorize注解进行资源的访问控制开启起来。 关键是将 prePostEnabled 设为 true @EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled=true) public class SecurityConfig extends WebSecurityConfigurerAdapter{//自定义的登录页面需要继承 重写里面的configure(HttpSecurity http) 这个HttpSecurity参数的 @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin()// 调用这个方法表示表单登录的意思,就像入门案例一样,就是一个表单登录。 // 告诉 spring security 自定义的登录页面是哪个,这个位置就是springboot静态资源目录里面的页面,一定要在授权那里放行这个页面 .loginPage("/login.html") // 这个写的是登录页面上提交用户名密码表单的那个action属性值(也就是html中<form>节点的action属性值)(合法的字符可以随便写) // 这个不用在Controller写对应的 Api 接口;只要求和html中<form>节点的action属性值一样就行。 .loginProcessingUrl("/login") // 登录成功后跳转的页面的接口Api,借助这个/toMain 的 Api 接口跳转到 main.html 页面。 // 不能直接写 main.html ,他要求必须是一个post请求才能跳转,直接写 main.html 会以Get请求跳转,会出现错误。所以要借助 Controller 的Api跳转 // 有的版本是 .defaultSuccessUrl("/toMain") 这个方法 .successForwardUrl("/toMain") // 登录失败(错误)后跳转的页面的接口Api,借助这个/toError 的 Api 接口跳转到 error.html 页面。 // 不能直接写 error.html ,他要求必须是一个post请求才能跳转,直接写 error.html 会以Get请求跳转,会出现错误。所以要借助 Controller 的Api跳转 // 记得下面授权放行,不然又被拦截回到登录页面 .failureForwardUrl("/toError"); http.authorizeRequests() .antMatchers("/error.html").permitAll() .antMatchers("/login.html").permitAll()// 登录页面放开权限,用户可以任意访问 .antMatchers("/js/**","/css/**","/images/**").permitAll()//.permitAll()这个后就是放行这些静态资源 //使用了@PreAuthorize注解进行资源的访问控制,就不要在配置类里配置权限相关的内容了,建议统一都用@PreAuthorize注解 //.antMatchers("/authorityTest.html").hasAuthority("权限1") //使用了@PreAuthorize注解进行资源的访问控制,就不要在配置类里配置权限相关的内容了,建议统一都用@PreAuthorize注解 //.antMatchers("/authorityTest.html").hasAnyAuthority("权限1","权限3") //除了放行的请求资源,其他所有请求都必须认证才能访问,必须登录 .anyRequest().authenticated(); //关闭csrf防护 http.csrf().disable(); } @Bean public PasswordEncoder getPw() { // 自定义登录逻辑的时候容器里面必须要有PasswordEncoder的实例 也就是我们不能直接new PasswordEncoder // 因为 Spring Security 内部会使用 PasswordEncoder 来和 UserDetailsService.loadUserByUsername(username)方法返回的 UserDetails 里面的密码做匹配校验 return new BCryptPasswordEncoder(); } }
-
在Controller中的方法上使用
@PreAuthorize
注解;注解的属性值写 Spring表达式(又叫SpEL表达式,通俗叫access表达式),因为这个注解就是基于表达式的访问控制,access表达式写法可以查看基于access方法的授权控制章节。例如:@PreAuthorize("hasRole('角色1')")、@PreAuthorize("hasRole('角色5') and hasIpAddress('192.168.1.100')")
。package com.xxx.springsecurity.demo.controller; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; /** * @Controller 和 @RestController 介绍 * * 当你使用 @RestController 注解时,默认情况下所有返回值都会被当作响应体,并且通常会被序列化为JSON或XML格式(取决于你的配置和返回对象类型)。 * * 当你使用 @Controller 注解时,编写的方法返回值是一个字符串时,并且没有使用 @ResponseBody 注解的情况会有下述的情况出现 * 这个字符串可以被视图解析器(如 Thymeleaf, JSP 等)解析为一个视图名称,则会尝试根据这个名称来查找并渲染相应的模板文件(可以理解为页面文件),从而实现页面跳转。 */ @Controller public class TestController { // 使用@PreAuthorize注解,然后基于角色的授权可以不用以 ROLE_ 开头 @PreAuthorize("hasRole('角色5')")//注解的属性值写 access 表达式;测试的时候调整这里的权限就好 @RequestMapping("/toMain")// 登录成功后通过这个接口Api跳转到 main.html页面。因为不能直接跳转页面 public String toMain() { System.out.println("ssssss"); return "redirect:main.html"; //重定向 /** * @Controller 和 @RestController 介绍 * * 当你使用 @RestController 注解时,默认情况下所有返回值都会被当作响应体,并且通常会被序列化为JSON或XML格式(取决于你的配置和返回对象类型)。 * 就算是字符串也会原封不动的作为响应体内容发送给客户端。 * * 当你使用 @Controller 注解时,编写的方法返回值是一个字符串时,并且没有使用 @ResponseBody 注解的情况会有下述的情况出现 * 这个字符串可以被视图解析器(如 Thymeleaf, JSP 等)解析为一个视图名称,则会尝试根据这个名称来查找并渲染相应的模板文件(可以理解为页面文件),从而实现页面跳转。 * 你可以显式地使用 RedirectView 或者 redirect: 前缀来执行重定向操作,例如返回 "redirect:/someUrl" 会触发302状态码,并告诉浏览器去访问 /someUrl 路径。 * 使用 forward: 前缀可以在服务器端转发请求到另一个URL,而不会改变浏览器地址栏中的URL。例如,返回 "forward:/anotherEndpoint"。 */ } @RequestMapping("/toError") public String toError() { System.out.println("ssssssa"); return "redirect:error.html"; //重定向 } }
-
测试:如果登录的用户拥有对应的角色权限就能正常的访问Api资源;如果登录的用户没有对应的角色权限将会报
org.springframework.security.access.AccessDeniedException: 不允许访问
异常;就算使用了自定义的403处理器(自定义的访问拒绝处理器)也一样会报异常。
3.8、自定义403处理方案
在 spriing security 中访问那些没有权限的资源它会跳到一个默认的错误提示页面,页面上提示出现错误并给出一个403的状态码。如下图所示:
在实际项目开发中,一般来说这种没有权限的的提示会自定义的去处理,不会去用它默认的处理方式。
下面将对自定义处理403的情况做出示例:
-
先按照基于权限的授权的内容把代码先写出来;关键的地方是改改配置类就好。修改的地方如下第2点所示;
-
在配置类中设置登录的用户没有权限;将
.antMatchers("/authorityTest.html").hasAnyAuthority("权限3")
这行代码写在配置类的对应地方,下面第4点的图片中有标注出所在的位置。 -
在
com.xxx.springsecurity.demo.handler
包中编写自定义403处理类(没有权限访问的处理类)package com.xxx.springsecurity.demo.handler; import java.io.IOException; import java.io.PrintWriter; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.stereotype.Component; @Component public class MyAccessDeniedHandler implements AccessDeniedHandler{ @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { response.setStatus(HttpServletResponse.SC_FORBIDDEN);//设置状态为403 response.setHeader("Content-Type", "application/json;charset=utf-8");//设置响应的数据是json PrintWriter writer = response.getWriter(); writer.write("{\"stutas\":403,\"msg\":\"权限不足,请联系管理员\"}");// 写一个json数据内容出去 writer.flush(); writer.close(); } }
-
在配置类中配置自定义的403处理类(没有权限访问的处理类)
-
测试结果…
四、退出登录
基于表单登录认证的退出比较简单,有内置的默认退出登录方式,如果不喜欢内置默认的退出登录还可以自定义的退出登录方式。
4.1、默认的退出登录
客户端请求执行/logout
这个url就能执行 Spring Security 内置默认的退出登录的逻辑。操作方式如下:
-
先按照自定义登录页面的内容把代码先写出来。
-
改一下main.html页面的代码,把退出登录的部分写在这个页面里。
<!DOCTYPE> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div>登录成功</div> <!-- 想要退出登录直接请求执行 /logout 这个url就能执行 Spring Security 内置默认的退出登录的逻辑 --> <!-- 退出成功会自动跳到登录页面 --> <div><a href="/logout">退出</a></div> <!-- 这里只是一个示例,在项目开发过程中以怎样的方式去触发 /logout 这个请求,由具体的需求来定。 --> </body> </html>
4.2、对退出登录做些自定义操作
先按照自定义登录页面的内容把代码先写出来。
改一下配置类:
package com.xxx.springsecurity.demo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter{//自定义的登录页面需要继承 重写里面的configure(HttpSecurity http) 这个HttpSecurity参数的
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()// 调用这个方法表示表单登录的意思,就像入门案例一样,就是一个表单登录。
// 告诉 spring security 自定义的登录页面是哪个,这个位置就是springboot静态资源目录里面的页面,一定要在授权那里放行这个页面
.loginPage("/login.html")
// 这个写的是登录页面上提交用户名密码表单的那个action属性值(也就是html中<form>节点的action属性值)(合法的字符可以随便写)
// 这个不用在Controller写对应的 Api 接口;只要求和html中<form>节点的action属性值一样就行。
.loginProcessingUrl("/login")
// 登录成功后跳转的页面的接口Api,借助这个/toMain 的 Api 接口跳转到 main.html 页面。
// 不能直接写 main.html ,他要求必须是一个post请求才能跳转,直接写 main.html 会以Get请求跳转,会出现错误。所以要借助 Controller 的Api跳转
// 有的版本是 .defaultSuccessUrl("/toMain") 这个方法
.successForwardUrl("/toMain")
// 登录失败(错误)后跳转的页面的接口Api,借助这个/toError 的 Api 接口跳转到 error.html 页面。
// 不能直接写 error.html ,他要求必须是一个post请求才能跳转,直接写 error.html 会以Get请求跳转,会出现错误。所以要借助 Controller 的Api跳转
// 记得下面授权放行,不然又被拦截回到登录页面
.failureForwardUrl("/toError");
http.authorizeRequests()
.antMatchers("/error.html").permitAll()
.antMatchers("/login.html").permitAll()// 登录页面放开权限,用户可以任意访问
//除了放行的请求资源,其他所有请求都必须认证才能访问,必须登录
.anyRequest()
.authenticated();
//退出登录 (这里只是一个示例;使用的时候结合项目的实际情况使用)
http.logout()
// 配置一下退出登录的url路径,不用 spring security 默认的。
// 这样配置之后默认退出的/logout的url路径就会改为/user/logout;使用时应该这样写:<a href="/user/logout">退出</a>
.logoutUrl("/user/logout")
// 退出成功后跳转的页面,spring security 默认退出登录成功后也是会跳转到/login.html,但是url路径上会带有?logout;例如:http://localhost:8081/login.html?logout。
// 在这里配置一个退出登录成功后跳转的页面就可以在url末尾不用携带?logout;而且可能在实际项目中退出登录后不让跳转登录页,也是可以在这里配置。
.logoutSuccessUrl("/login.html")
// 下面两个方法就不在这介绍了。
//.logoutSuccessHandler() // 退出成功处理器,要写一个类,类里面写退出成功的处理逻辑,退出后有什么特别需要定制的逻辑可以写在这里面
//.logoutRequestMatcher() // 从方法名上来看 退出请求匹配,不知道干什么的。
;
//关闭csrf防护
http.csrf().disable();
}
@Bean
public PasswordEncoder getPw() {
// 自定义登录逻辑的时候容器里面必须要有PasswordEncoder的实例 也就是我们不能直接new PasswordEncoder
// 因为 Spring Security 内部会使用 PasswordEncoder 来和 UserDetailsService.loadUserByUsername(username)方法返回的 UserDetails 里面的密码做匹配校验
return new BCryptPasswordEncoder();
}
}
他在退出的过程中会清除 session 还有 Authentication(SecurityContextHolder.getContext().setAuthentication(null); SecurityContextHolder.clearContext();
清除整个上下文。)
第一个默认退出登录的url是/logout;默认退出成功之后跳转的url是/login?logout
,实际上它是做重定向操作。
第二个它正儿八经退出的时候除了重定向之外还做了两件事,第一个销毁HttpSession对象,第二个清除Authentication对象
五、记住登录、自动登录(Remember-me)
Spring Security中的 Remember-me 可以做到一种记住登录的效果,也可以称为自动登录吧,效果就是关闭了浏览器然后重新打开浏览器再打开原来需要登录才能访问的url地址,它不会再次让你去登录了;你关闭了后端的服务再次重启依然不需要你重新登录就能访问资源。
操作如下:
-
引入依赖
<?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> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.1.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.xxx</groupId> <artifactId>spring_security_rememberme_demo</artifactId> <version>0.0.1-SNAPSHOT</version> <name>spring_security_demo16</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <!-- security组件 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- web组件 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> <!-- Mybatis依赖 --> <!-- spring security实现Remember Me功能时,底层实现依赖依赖spring jdbc,所以需要导入Spring jdbc。 项目开发中以后大多使用Mybatis框架,很少直接导入spring jdbc,所以此处导入mybatis --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.1</version> </dependency> <!-- MySql数据库依赖;根据MySql的版本将配置文件中的驱动,url写正确,不然启动会报错 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.38</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
-
编写spring boot的配置文件;也就是配置一下数据源
server: port: 8081 spring: datasource: #driver-class-name: com.mysql.jdbc.Driver MySql5 的驱动写法 driver-class-name: com.mysql.jdbc.Driver #url: MySql5 的url写法和 MySql8 的写法也有一些区别,但是MySql8的写法可以用于 MySql5; 但是使用 MySql5 的url写法用于 MySql8 会有问题 url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 username: root password: root #一定要要根据Mysql数据库版本正确的编写驱动和URL,不然会启动报错
-
编写UserDetailService的实现类
package com.xxx.springsecurity.demo.service; 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.stereotype.Service; @Service public class UserDetailServiceImpl implements UserDetailsService{ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //1、根据用户名去数据库查询 ,如果不存在就抛UsernameNotFoundException。 if(!"admin".equals(username)) {// 为了方便这里就不建数据库表了,也就不查数据库了;就假设数据库中只有 admin 这个用户 throw new UsernameNotFoundException("用户不存在"); } //2、设置一个密码,让 SpringSecurity 内部比较密码 String password = "$2a$10$ODk4SFGfaikJDhHSaxJGg.I0rmPvBUaYcxpivb9HayKtcAzU/EYge";//假设这个是数据库的密码 // 这个密码的原文就是123 也就是使用 passwordEncoder.encode("123"); 得到的密码密文。 //AuthorityUtils 是一个权限工具类,他底层就是通过,号分割的 return new User(username,password,AuthorityUtils.commaSeparatedStringToAuthorityList("权限1,权限2,ROLE_角色1,ROLE_角色2,/main.html")); // 这里只需要返回一个包含用户名、权限、密文密码的 UserDetails 的实现类就行了。 // 密码的比较校验和权限的检验都由 Spring Security 内部的逻辑来进行 } }
-
编写Spring Security配置类
package com.xxx.springsecurity.demo.config; import javax.sql.DataSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl; import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository; import com.xxx.springsecurity.demo.service.UserDetailServiceImpl; @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter{//自定义的登录页面需要继承 重写里面的configure(HttpSecurity http) 这个HttpSecurity参数的 @Autowired private DataSource dataSource;//为什么能注入,因为引入了相关的依赖,并且yml都配置了跟数据源相关的配置,spring boot 就会自动配置 @Autowired private PersistentTokenRepository persistentTokenRepository;// 这个类就是下面74行所配置的那个Bean @Autowired private UserDetailServiceImpl userDetailServiceImpl; @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin()// 调用这个方法表示表单登录的意思,就像入门案例一样,就是一个表单登录。 // 告诉 spring security 自定义的登录页面是哪个,这个位置就是springboot静态资源目录里面的页面,一定要在授权那里放行这个页面 .loginPage("/login.html") // 这个写的是登录页面上提交用户名密码表单的那个action属性值(也就是html中<form>节点的action属性值)(合法的字符可以随便写) // 这个不用在Controller写对应的 Api 接口;只要求和html中<form>节点的action属性值一样就行。 .loginProcessingUrl("/login") // 登录成功后跳转的页面的接口Api,借助这个/toMain 的 Api 接口跳转到 main.html 页面。 // 不能直接写 main.html ,他要求必须是一个post请求才能跳转,直接写 main.html 会以Get请求跳转,会出现错误。所以要借助 Controller 的Api跳转 // 有的版本是 .defaultSuccessUrl("/toMain") 这个方法 .successForwardUrl("/toMain") // 登录失败(错误)后跳转的页面的接口Api,借助这个/toError 的 Api 接口跳转到 error.html 页面。 // 不能直接写 error.html ,他要求必须是一个post请求才能跳转,直接写 error.html 会以Get请求跳转,会出现错误。所以要借助 Controller 的Api跳转 // 记得下面授权放行,不然又被拦截回到登录页面 .failureForwardUrl("/toError"); http.authorizeRequests() .antMatchers("/error.html").permitAll() .antMatchers("/login.html").permitAll()// 登录页面放开权限,用户可以任意访问 .antMatchers("/js/**","/css/**","/images/**").permitAll()//.permitAll()这个后就是放行这些静态资源 //权限控制,严格区分大小写。登录成功的时候用户所带有的权限。有这个权限就能访问这个资源 //.antMatchers("/authorityTest.html").hasAuthority("权限1") //也是权限控制的一种,严格区分大小写。登录成功的时候用户所带有的权限。 如果这个人没有权限3.但是却有权限1还是能访问这个资源的 .antMatchers("/authorityTest.html").hasAnyAuthority("权限1","权限3") .anyRequest().authenticated(); //记住我 http.rememberMe() .tokenRepository(persistentTokenRepository)//存放的数据源,把用户的令牌或者信息存起来 token;这里使用 JdbcTokenRepositoryImpl 基于数据库存储的方式; //.rememberMeParameter(rememberMeParameter)// 类似于自定义登录入参 设置remember-me 为自定义的 .tokenValiditySeconds(60)//设置超时时间60秒 默认是两周 .userDetailsService(userDetailServiceImpl);//自定义登录逻辑 //关闭csrf防护 http.csrf().disable(); } @Bean public PersistentTokenRepository persistentTokenRepository() { /** * PersistentTokenRepository是一个接口,得找他得实现类. * * PersistentTokenRepository 在当前版本有两个内置的实现类: * 1、JdbcTokenRepositoryImpl 基于数据库存储的方式; * 2、InMemoryTokenRepositoryImpl 基于内存的存储方式; * * 这里介绍的是基于数据库存储的方式; */ JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl(); //设置数据源 jdbcTokenRepository.setDataSource(dataSource); //自动建表,第一次启动时开启,第二次启动时注释掉,因为第一次已经建表了 //jdbcTokenRepository.setCreateTableOnStartup(true); return jdbcTokenRepository; } @Bean public PasswordEncoder getPw() { // 自定义登录逻辑的时候容器里面必须要有PasswordEncoder的实例 也就是我们不能直接new PasswordEncoder // 因为 Spring Security 内部会使用 PasswordEncoder 来和 UserDetailsService.loadUserByUsername(username)方法返回的 UserDetails 里面的密码做匹配校验 return new BCryptPasswordEncoder(); } }
-
编写Controller
package com.xxx.springsecurity.demo.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; /** * @Controller 和 @RestController 介绍 * * 当你使用 @RestController 注解时,默认情况下所有返回值都会被当作响应体,并且通常会被序列化为JSON或XML格式(取决于你的配置和返回对象类型)。 * * 当你使用 @Controller 注解时,编写的方法返回值是一个字符串时,并且没有使用 @ResponseBody 注解的情况会有下述的情况出现 * 这个字符串可以被视图解析器(如 Thymeleaf, JSP 等)解析为一个视图名称,则会尝试根据这个名称来查找并渲染相应的模板文件(可以理解为页面文件),从而实现页面跳转。 */ @Controller public class TestController { @RequestMapping("/toMain")// 登录成功后通过这个接口Api跳转到 main.html页面。因为不能直接跳转页面 public String toMain() { System.out.println("ssssss"); return "redirect:main.html"; //重定向 /** * @Controller 和 @RestController 介绍 * * 当你使用 @RestController 注解时,默认情况下所有返回值都会被当作响应体,并且通常会被序列化为JSON或XML格式(取决于你的配置和返回对象类型)。 * 就算是字符串也会原封不动的作为响应体内容发送给客户端。 * * 当你使用 @Controller 注解时,编写的方法返回值是一个字符串时,并且没有使用 @ResponseBody 注解的情况会有下述的情况出现 * 这个字符串可以被视图解析器(如 Thymeleaf, JSP 等)解析为一个视图名称,则会尝试根据这个名称来查找并渲染相应的模板文件(可以理解为页面文件),从而实现页面跳转。 * 你可以显式地使用 RedirectView 或者 redirect: 前缀来执行重定向操作,例如返回 "redirect:/someUrl" 会触发302状态码,并告诉浏览器去访问 /someUrl 路径。 * 使用 forward: 前缀可以在服务器端转发请求到另一个URL,而不会改变浏览器地址栏中的URL。例如,返回 "forward:/anotherEndpoint"。 */ } @RequestMapping("/toError") public String toError() { System.out.println("ssssssa"); return "redirect:error.html"; //重定向 } }
-
登录页面,页面的代码写在
src/main/resources/static/login.html
<!DOCTYPE> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <form action="/login" method="post"> 用户名:<input type="text" name="username"> <br/> 密码:<input type="password" name="password"><br/> <!-- 登录时添加remember-me复选框;name="remember-me"是spring security框架内部默认的,也可以在配置类中修改。 --> 记住我:<input type="checkbox" name="remember-me" value="true"/> <br/> <input type="submit" value="提交"> </form> </body> </html>
-
登录成功后的 main.html 页面,页面的代码写在
src/main/resources/static/main.html
<!DOCTYPE> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div>登录成功</div> <!-- 其实和上面介绍的自定义登录页面的代码逻辑就多了这一行 这一行的代码是为了让登录成功后直接点击这里跳转一个页面来进行测试授权权限是否起效果。 --> <div><a href="authorityTest.html">跳转-authorityTest</a></div> </body> </html>
-
在 /src/main/resources/static 目录下(也就是在main.html页面同一个目录下)创建一个 authorityTest.html
<!DOCTYPE> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div>AuthorityTest.权限控制-测试</div> </body> </html>
-
写一个error.html页面,页面的代码写在
src/main/resources/static/error.html
<!DOCTYPE> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div>操作失败,请重新登录<a href="/login.html">跳转</a></div> </body> </html>
-
解释说明
六、spring security 在 Thymeleaf 视图技术中的使用
Spring Security 可以在一些视图技术中可以控制一些信息数据的显示和一些操作交互。例如:可以在 JSP 或者 thymeleaf 中可以控制数据的显示或者不显示,还可以控制页面上某些按钮可以点击或者不能点击,还可以控制按钮显示或者不显示。
这里将介绍 Spring boot 整合 Spring Security 结合着 Thymeleaf 视图技术来控制页面信息显示的示例;选取 Thymeleaf 作为视图展示技术也是因为使用了 Spring boot 技术并且前后端不分离的项目中更多的都是使用 Thymeleaf 作为视图展示技术。
代码结构示例
使用操作如下:
Thymeleaf对spring security的支持都放在 thymeleaf-extras-springsecurityX 中, X代表版本,目前最新为5; 所以需要在项目中添加此jar包的依赖和Thymeleaf的依赖
-
引入依赖
<?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> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.1.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.xxx</groupId> <artifactId>spring_security_demo17</artifactId> <version>0.0.1-SNAPSHOT</version> <name>spring_security_demo17</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- thymeleaf springsecurity5 依赖--> <dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity5</artifactId> </dependency> <!-- thymeleaf依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
-
编写Spring Security配置类
package com.xxx.springsecurity.demo.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter{//自定义的登录页面需要继承 重写里面的configure(HttpSecurity http) 这个HttpSecurity参数的 @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin()// 调用这个方法表示表单登录的意思,就像入门案例一样,就是一个表单登录。 // 告诉 spring security 自定义的登录页面是哪个,这个位置就是springboot静态资源目录里面的页面,一定要在授权那里放行这个页面 .loginPage("/login.html") // 这个写的是登录页面上提交用户名密码表单的那个action属性值(也就是html中<form>节点的action属性值)(合法的字符可以随便写) // 这个不用在Controller写对应的 Api 接口;只要求和html中<form>节点的action属性值一样就行。 .loginProcessingUrl("/login") // 登录成功后跳转的页面的接口Api,借助这个/toMain 的 Api 接口跳转到 main.html 页面。 // 不能直接写 main.html ,他要求必须是一个post请求才能跳转,直接写 main.html 会以Get请求跳转,会出现错误。所以要借助 Controller 的Api跳转 // 有的版本是 .defaultSuccessUrl("/toMain") 这个方法 .successForwardUrl("/toMain") // 登录失败(错误)后跳转的页面的接口Api,借助这个/toError 的 Api 接口跳转到 error.html 页面。 // 不能直接写 error.html ,他要求必须是一个post请求才能跳转,直接写 error.html 会以Get请求跳转,会出现错误。所以要借助 Controller 的Api跳转 // 记得下面授权放行,不然又被拦截回到登录页面 .failureForwardUrl("/toError"); http.authorizeRequests() .antMatchers("/error.html").permitAll() .antMatchers("/login.html").permitAll()// 登录页面放开权限,用户可以任意访问 //除了放行的请求资源,其他所有请求都必须认证才能访问,必须登录 .anyRequest() .authenticated(); //关闭csrf防护 http.csrf().disable(); } @Bean public PasswordEncoder getPw() { // 自定义登录逻辑的时候容器里面必须要有PasswordEncoder的实例 也就是我们不能直接new PasswordEncoder // 因为 Spring Security 内部会使用 PasswordEncoder 来和 UserDetailsService.loadUserByUsername(username)方法返回的 UserDetails 里面的密码做匹配校验 return new BCryptPasswordEncoder(); } }
-
编写UserDetailService的实现类
package com.xxx.springsecurity.demo.service; import org.springframework.beans.factory.annotation.Autowired; 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.password.PasswordEncoder; import org.springframework.stereotype.Service; @Service public class UserDetailServiceImpl implements UserDetailsService{ //@Autowired //private PasswordEncoder passwordEncoder; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //1、根据用户名去数据库查询 ,如果不存在就抛UsernameNotFoundException。 if(!"admin".equals(username)) {// 为了方便这里就不建数据库表了,也就不查数据库了;就假设数据库中只有 admin 这个用户 throw new UsernameNotFoundException("用户不存在"); } //2、设置一个密码,让 SpringSecurity 内部比较密码 String password = "$2a$10$ODk4SFGfaikJDhHSaxJGg.I0rmPvBUaYcxpivb9HayKtcAzU/EYge";//假设这个是数据库的密码 // 这个密码的原文就是123 也就是使用 passwordEncoder.encode("123"); 得到的密码密文。 //AuthorityUtils 是一个权限工具类,他底层就是通过,号分割的 return new User(username,password,AuthorityUtils.commaSeparatedStringToAuthorityList("权限1,权限2,ROLE_角色1,ROLE_角色2")); // 这里只需要返回一个包含用户名、权限、密文密码的 UserDetails 的实现类就行了。 // 密码的比较校验和权限的检验都由 Spring Security 内部的逻辑来进行 } }
-
编写Controller类,然后在这个Controller类中编写一些接口Api。
package com.xxx.springsecurity.demo.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping; /** * @Controller 和 @RestController 介绍 * * 当你使用 @RestController 注解时,默认情况下所有返回值都会被当作响应体,并且通常会被序列化为JSON或XML格式(取决于你的配置和返回对象类型)。 * * 当你使用 @Controller 注解时,编写的方法返回值是一个字符串时,并且没有使用 @ResponseBody 注解的情况会有下述的情况出现 * 这个字符串可以被视图解析器(如 Thymeleaf, JSP 等)解析为一个视图名称,则会尝试根据这个名称来查找并渲染相应的模板文件(可以理解为页面文件),从而实现页面跳转。 */ @Controller public class TestController { @GetMapping("/test01")// 通过这个 Api 接口让程序跳转到用Thymeleaf技术编写的 demo.html 页面。 public String demo01() { return "demo"; } @RequestMapping("/toMain")// 登录成功后通过这个接口Api跳转到 main.html页面。因为不能直接跳转页面 public String toMain() { System.out.println("ssssss"); return "redirect:main.html"; //重定向 /** * @Controller 和 @RestController 介绍 * * 当你使用 @RestController 注解时,默认情况下所有返回值都会被当作响应体,并且通常会被序列化为JSON或XML格式(取决于你的配置和返回对象类型)。 * 就算是字符串也会原封不动的作为响应体内容发送给客户端。 * * 当你使用 @Controller 注解时,编写的方法返回值是一个字符串时,并且没有使用 @ResponseBody 注解的情况会有下述的情况出现 * 这个字符串可以被视图解析器(如 Thymeleaf, JSP 等)解析为一个视图名称,则会尝试根据这个名称来查找并渲染相应的模板文件(可以理解为页面文件),从而实现页面跳转。 * 你可以显式地使用 RedirectView 或者 redirect: 前缀来执行重定向操作,例如返回 "redirect:/someUrl" 会触发302状态码,并告诉浏览器去访问 /someUrl 路径。 * 使用 forward: 前缀可以在服务器端转发请求到另一个URL,而不会改变浏览器地址栏中的URL。例如,返回 "forward:/anotherEndpoint"。 */ } @RequestMapping("/toError") public String toError() { System.out.println("ssssssa"); return "redirect:error.html"; //重定向 } }
-
关于 error.html、login.html、main.html 页面根据自定义登录页面的代码的写法来写。点击这里查看
-
在
src/main/resources/templates
目录下编写Thymeleaf语法的demo.html页面,没有templates目录就添加上templates目录<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5"> <!-- 在html中引入Thymeleaf的命名空间和security的命名空间, xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5" (命名空间中 th表示Thymeleaf sec表示security) 使用了 sec命名空间 页面上就能展示 Spring Security 中的 UsernamePasswordAuthenticationToken这个类里面的东西 也能通过 sec命名空间 页面上就能使用 Spring Security 的权限控制功能,从而就能控制页面上的信息显示,或者一些操作功能的显示。 --> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <!-- 可以在html页面中通过 sec:authentication="xxx" 获取 UsernamePasswordAuthenticationToken 中所有 getxxx()的内容, 也包括UsernamePasswordAuthenticationToken的父类中的 getxxx()的内容 根据UsernamePasswordAuthenticationToken的源码得到以下属性 name:登录账号名称 principal:登录主体,也就是自定义登录逻辑中的那个UserDetails的实现类 credentials:凭证,也就是密码。(为了安全这个是获取不到的,为null) authorities:权限和角色 details:实际是WebAutnenticationDetails的实例。可以获取 remoteAddress (客户端IP) 和sessionId(当前sessionId) --> 登录账号:<span sec:authentication="name"></span> <br/> 登录账号:<span sec:authentication="principal.username"></span><br/> 凭证:<span sec:authentication="credentials"></span> <br/><!-- 基于安全考虑输出会是一个null --> 权限和角色:<span sec:authentication="authorities"></span><br/> 客户端地址:<span sec:authentication="details.remoteAddress"></span><br/> sessionId:<span sec:authentication="details.sessionId"></span> <br/> <hr/> <!-- 可以在html页面中通过 sec:authorize="SpEl (也就是所谓的access表达式)" 做权限判断,来控制页面所需要显示的内容 --> 通过权限判断: <button sec:authorize="hasAuthority('权限1')">新增</button> <button sec:authorize="hasAuthority('权限2')">修改</button> <button sec:authorize="hasAuthority('权限3')">删除</button> <button sec:authorize="hasAuthority('权限4')">查看</button> <br/> 通过角色判断: <button sec:authorize="hasRole('角色1')">角色</button> <button sec:authorize="hasRole('角色1')">角色</button> <button sec:authorize="hasRole('角色1')">角色</button> <button sec:authorize="hasRole('角色1')">角色</button> <button sec:authorize="hasRole('角色1')">角色</button> </body> </html>
-
测试:登录认证了之后,直接在浏览器的地址栏输入
http://localhost:8081/test01
就能看到效果,或者在main页面放个<a href="/test01">跳转</a>
七、csrf(跨站请求伪造)
7.1、介绍
跨站请求伪造(CSRF)是一种网络安全漏洞,它允许攻击者诱导受害者在已认证的网络应用程序中执行非预期的操作。CSRF 攻击通常发生在用户已经登录某个网站的情况下,攻击者利用用户的登录状态在用户不知情的情况下发送恶意请求,从而实现非法操作,例如转账、修改用户信息等。
简单的说:用户在某个网站进行了登录认证,又在同一个浏览器上打开其他网站、恶意网站(这个打开其他网站、恶意网站的操作可能是用户自己不知情的情况下打开的,又或者可能是点击了某些广告弹窗被诱导点击了打开了某些恶意网站,又或者用户不知情的情况下乱点击了一些什么链接之类的打开了某些恶意网站),恶意的网站能够访问到或者伪造到已经登录认证过的那个目标网站里边一些内容,这样就会对已经登录认证过的那个目标网站会造成一些攻击性的事件发生,这个是很不安全的。
7.2、例子
例如,假设用户已经登录了银行网站 http://bank.example
,然后访问了一个恶意网站。恶意网站包含一段代码,自动向 http://bank.example
发送一个转账请求。用户的浏览器在发送这个请求时会附带之前登录银行网站时保存的 Cookie,因此银行网站会认为这个请求是合法的,从而执行转账操作。
详细说明:
-
用户在浏览器登录了某个银行的网站
http://bank.example
,这个网站有一个用于转账的url请求http://bank.example/transfer?to=登录用户的账号&amount=10000
-
用户又在同一个浏览器上打开其他网站、恶意网站(这个打开其他网站、恶意网站的操作可能是用户自己不知情的情况下打开的,又或者可能是点击了某些广告弹窗被诱导点击了打开了某些恶意网站,又或者用户不知情的情况下乱点击了一些什么链接之类的打开了某些恶意网站);这个恶意网站会加载一个图片,这个图片的代码为
<img src="http://bank.example/transfer?to=攻击者账号&amount=10000">
,这个其他网站、恶意网站打开时浏览器会自动加载这个<img>
图片,<img>中的src的url就会被执行,(因为加载图片就会执行这个url,学过html的都知道)
这时候就会有10000的钱转到了攻击者的账户中去了。
7.3、原理
CSRF 攻击的核心在于利用用户的登录状态。当用户登录某个网站后,浏览器会保存用户的会话信息(如 Cookie)。在用户访问其他恶意网站时,恶意网站可以发送一个请求到之前用户已登录的网站,而用户的浏览器在发送这个请求时会自动附带用户的会话信息(如Cookie信息),从而使得请求看起来是合法的(服务器会认为这个请求是合法的)。
- 攻击条件:
- 用户已登录:目标用户必须已登录目标网站(如银行网站),且会话(Session)未过期。
- 诱导访问恶意请求:用户被诱导访问攻击者构造的恶意页面或链接,触发对目标网站的请求。
- 攻击流程:
- 用户登录:用户登录目标网站(如银行),网站生成 Cookie 并存储在用户浏览器中。
- 触发恶意请求:用户访问攻击者构造的恶意页面,页面中的代码(如
<img>
标签、表单等)会自动向目标网站发送请求。 - 浏览器自动携带 Cookie:由于用户已登录,浏览器会自动携带有效 Cookie 发送请求,目标网站无法区分请求是否用户主动发起。
- 执行恶意操作:目标网站将请求视为合法操作,执行转账、修改密码等行为。
7.4、危害
CSRF 攻击的危害主要体现在以下几个方面:
- 用户信息泄露:攻击者可以利用 CSRF 攻击获取用户的敏感信息,如账户余额、交易记录、手机号码、邮箱等。
- 非法恶意操作:攻击者可以非法操作,如转账、修改密码、虚拟货币交易等,从而给用户带来经济损失。
- 系统权限提升:在某些情况下,攻击者可以通过 CSRF 攻击提升自己的系统权限,从而进一步控制用户的账户或系统;比如在社交平台发布恶意内容,让非法的内容通过审批然后进行发布。
7.5、防御措施
为了防御 CSRF 攻击,可以采取以下措施:
-
CSRF Token(核心防御):
-
原理:在用户每个请求中添加一个随机生成且难以预测的 Token,并在服务器端进行验证。如果 Token 不匹配,则认为请求不合法。
-
实现方式:
- 表单提交:在表单中添加隐藏字段(如
"_csrf"
)。 - AJAX 请求:将 Token 放在请求头(如
X-CSRF-TOKEN
)或请求体中。
- 表单提交:在表单中添加隐藏字段(如
-
Spring Security 中的实现:
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .csrf() // 启用 CSRF 防护 .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()); } }
-
-
SameSite Cookie 属性:
-
作用:限制 Cookie 在跨站请求中的发送。
-
模式:
- Strict:仅允许同源请求携带 Cookie(严格模式,安全性高)。
- Lax:允许跨站的 GET 请求携带 Cookie,但阻止非 GET 请求(如 POST)。
- None:需配合
Secure
属性(仅 HTTPS),否则无效。
-
配置示例:
Java浅色版本
@Bean public CookieCsrfTokenRepository csrfTokenRepository() { CookieCsrfTokenRepository repository = new CookieCsrfTokenRepository(); repository.setCookieHttpOnly(true); repository.setCookieSameSite("Strict"); // 设置为 Strict 模式 return repository; }
-
-
验证 HTTP Referer 头:
- 通过检查 HTTP 请求的 Referer 头来判断请求是否来自合法的来源,检查请求来源是否为可信域名(需注意 Referer 可能被伪造)。
-
限制请求方法:
- 仅允许 GET 请求获取资源,敏感操作(如转账)需通过 POST/PUT 等方法,并结合 Token 验证。
-
使用验证码:在关键操作(如转账)中添加验证码验证,增加攻击的难度。
-
设置 HttpOnly Cookie:通过设置 Cookie 的 HttpOnly 属性,防止 JavaScript 代码读取 Cookie 信息,从而降低 CSRF 攻击的风险。
-
用户教育:提醒用户不要同时登录多个网站,避免访问不可信的网站,以减少 CSRF 攻击的可能性。
7.6、与 XSS 的区别
-
XSS(跨站脚本):
- 利用用户对网站的信任,注入恶意脚本窃取用户数据(如 Cookie)。
- 攻击目标是“获取用户数据”。
-
CSRF:
- 利用网站对用户浏览器的信任,伪造请求执行操作。
-
攻击目标是“挟持用户身份执行恶意操作”。
7.7、Spring Security 基于CSRF Token 防护示例
这里将使用 spring boot + spring security + thymeleaf 的技术作为使用csrf保护的示例。一般来说,像这样的前后端不分离方式还是应该开启CSRF防护。
关键的地方就是注释掉 http.csrf().disable();
这一行代码就会默认开启csrf防护;也就是在spring security配置类中不要出现http.csrf().disable();
这一行代码,还有需要在thymeleaf的登录页面登录的表单加上 <input type="hidden" name="_csrf" th:value="${_csrf.token}" th:if="${_csrf}">
-
引入依赖
<?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> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.1.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.xxx</groupId> <artifactId>spring_security_demo_csrf</artifactId> <version>0.0.1-SNAPSHOT</version> <name>spring_security_demo_csrf</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <!-- security组件 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- web组件 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- thymeleaf依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
-
编写UserDetailsService实现类
package com.xxx.springsecurity.demo.service; import org.springframework.beans.factory.annotation.Autowired; 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.password.PasswordEncoder; import org.springframework.stereotype.Service; @Service public class UserDetailServiceImpl implements UserDetailsService{ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //1、根据用户名去数据库查询 ,如果不存在就抛UsernameNotFoundException。 if(!"admin".equals(username)) {// 为了方便这里就不建数据库表了,也就不查数据库了;就假设数据库中只有 admin 这个用户 throw new UsernameNotFoundException("用户不存在"); } //2、设置一个密码,让 SpringSecurity 内部比较密码 String password = "$2a$10$ODk4SFGfaikJDhHSaxJGg.I0rmPvBUaYcxpivb9HayKtcAzU/EYge";//假设这个是数据库的密码 // 这个密码的原文就是123 也就是使用 passwordEncoder.encode("123"); 得到的密码密文。 //AuthorityUtils 是一个权限工具类,他底层就是通过,号分割的 return new User(username,password,AuthorityUtils.commaSeparatedStringToAuthorityList("权限1,权限2,ROLE_角色1,ROLE_角色2")); // 这里只需要返回一个包含用户名、权限、密文密码的 UserDetails 的实现类就行了。 // 密码的比较校验和权限的检验都由 Spring Security 内部的逻辑来进行 } }
-
编写Spring Security配置类
package com.xxx.springsecurity.demo.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter{//自定义的登录页面需要继承 重写里面的configure(HttpSecurity http) 这个HttpSecurity参数的 @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin()// 调用这个方法表示表单登录的意思,就像入门案例一样,就是一个表单登录。 // 告诉 spring security 自定义的登录页面是哪个,一定要在授权那里放行这个Api接口 //这个 /showLogin 是一个需要自己写的Api接口,利用这个Api接口会跳转到templates里面的login.html。 .loginPage("/showLogin") // 这个写的是登录页面上提交用户名密码表单的那个action属性值(也就是html中<form>节点的action属性值)(合法的字符可以随便写) // 这个不用在Controller写对应的 Api 接口;只要求和html中<form>节点的action属性值一样就行。 .loginProcessingUrl("/login") // 登录成功后跳转的页面的接口Api,借助这个/toMain 的 Api 接口跳转到templates里面的 main.html 页面。 // 不能直接写 main.html ,他要求必须是一个post请求才能跳转,直接写 main.html 会以Get请求跳转,会出现错误。所以要借助 Controller 的Api跳转 // 有的版本是 .defaultSuccessUrl("/toMain") 这个方法 .successForwardUrl("/toMain") // 登录失败(错误)后跳转的页面的接口Api,借助这个/toError 的 Api 接口跳转到templates里面的 error.html 页面。 // 不能直接写 error.html ,他要求必须是一个post请求才能跳转,直接写 error.html 会以Get请求跳转,会出现错误。所以要借助 Controller 的Api跳转 .failureForwardUrl("/loginError"); http.authorizeRequests() .antMatchers("/showLogin").permitAll()//放行showLogin接口 .antMatchers("/js/**","/css/**","/images/**").permitAll()//.permitAll()这个后就是放行这些静态资源 //权限控制,严格区分大小写。登录成功的时候用户所带有的权限。有这个权限就能访问这个资源 //.antMatchers("/authorityTest.html").hasAuthority("权限1") //也是权限控制的一种,严格区分大小写。登录成功的时候用户所带有的权限。 如果这个人没有权限3.但是却有权限1还是能访问这个资源的 .antMatchers("/authorityTest").hasAnyAuthority("权限1","权限3") //除了放行的请求资源,其他所有请求都必须认证才能访问,必须登录 .anyRequest().authenticated(); //根据项目需要配置一个异常处理器,就像前面说的自定义403处理方案那样。自定义的异常处理是有较多内容要介绍,这里就不写这个代码了。 //...... //退出 http.logout() //.logoutUrl("/user/logout") 这样的默认退出的/logout的url路径就会改为/user/logout <a href="/user/logout">退出</a> .logoutSuccessUrl("/showLogin");//退出成功后跳转的页面 //关闭csrf防护 //从Spring Security 4.0 开始,默认情况下会启用CSRF保护,以防止CSRF攻击应用程序,Spring Security CSRF会针对patch、post、put、和delete的请求进行保护。 //http.csrf().disable(); 注释掉就会默认开启csrf防护 //注释掉 http.csrf().disable(); 这一行代码就会默认开启csrf防护 //注释掉 http.csrf().disable(); 这一行代码就会默认开启csrf防护 //注释掉 http.csrf().disable(); 这一行代码就会默认开启csrf防护 //注释掉 http.csrf().disable(); 这一行代码就会默认开启csrf防护 } @Bean public PasswordEncoder getPw() { // 自定义登录逻辑的时候容器里面必须要有PasswordEncoder的实例 也就是我们不能直接new PasswordEncoder // 因为 Spring Security 内部会使用 PasswordEncoder 来和 UserDetailsService.loadUserByUsername(username)方法返回的 UserDetails 里面的密码做匹配校验 return new BCryptPasswordEncoder(); } }
-
编写controller类
package com.xxx.springsecurity.demo.controller; import java.util.HashMap; import java.util.Map; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; /** * @Controller 和 @RestController 介绍 * * 当你使用 @RestController 注解时,默认情况下所有返回值都会被当作响应体,并且通常会被序列化为JSON或XML格式(取决于你的配置和返回对象类型)。 * * 当你使用 @Controller 注解时,编写的方法返回值是一个字符串时,并且没有使用 @ResponseBody 注解的情况会有下述的情况出现 * 这个字符串可以被视图解析器(如 Thymeleaf, JSP 等)解析为一个视图名称,则会尝试根据这个名称来查找并渲染相应的模板文件(可以理解为页面文件),从而实现页面跳转。 */ @Controller public class TestController { @RequestMapping("/toMain")// 登录成功后通过这个接口Api跳转到 main.html页面。因为不能直接跳转页面 public String toMain() { System.out.println("ssssss"); return "main"; /** * @Controller 和 @RestController 介绍 * * 当你使用 @RestController 注解时,默认情况下所有返回值都会被当作响应体,并且通常会被序列化为JSON或XML格式(取决于你的配置和返回对象类型)。 * 就算是字符串也会原封不动的作为响应体内容发送给客户端。 * * 当你使用 @Controller 注解时,编写的方法返回值是一个字符串时,并且没有使用 @ResponseBody 注解的情况会有下述的情况出现 * 这个字符串可以被视图解析器(如 Thymeleaf, JSP 等)解析为一个视图名称,则会尝试根据这个名称来查找并渲染相应的模板文件(可以理解为页面文件),从而实现页面跳转。 * 你可以显式地使用 RedirectView 或者 redirect: 前缀来执行重定向操作,例如返回 "redirect:/someUrl" 会触发302状态码,并告诉浏览器去访问 /someUrl 路径。 * 使用 forward: 前缀可以在服务器端转发请求到另一个URL,而不会改变浏览器地址栏中的URL。例如,返回 "forward:/anotherEndpoint"。 */ } @RequestMapping("/loginError") public String toError() { System.out.println("loginerror"); return "loginerror"; } @GetMapping("/showLogin") public String showLogin() { return "login";//用了模板引擎 直接就能这么跳转。跳转到的地方是templates里面的login.html } @PostMapping("/authorityTest") public String authorityTest() { return "authorityTest";//用了模板引擎 直接就能这么跳转。跳转到的地方是templates里面的authorityTest.html } @ResponseBody @GetMapping("/test01") public Map<String,Object> test01() { Map<String,Object> map = new HashMap<String,Object>(); map.put("a", 1); map.put("b", 2); return map; } }
-
在
src/main/resources/templates
目录下创建login.html页面。<!DOCTYPE> <html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div>这是templates里面的login</div> <form action="/login" method="post"> <!-- 这一行代码很关键;这个name="_csrf"是默认的固定写法,除非在配置类自定义。 --> <input type="hidden" name="_csrf" th:value="${_csrf.token}" th:if="${_csrf}"> 用户名:<input type="text" name="username"> <br/> 密码:<input type="password" name="password"> <br/> <input type="submit" value="提交"> </form> </body> </html>
-
在
src/main/resources/templates
目录下创建main.html页面。<!DOCTYPE> <html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div>登录成功</div> <p>------------------------------------------------------------</p> <div>csrf_token:<span th:text = "${_csrf.token}"></span></div> <p>------------------------------------------------------------</p> <div><a href="/logout">退出</a>---- a标签方式调用退出是会以 get 方式提交,开启了csrf防护将不能正常退出。</div> <p>------------------------------------------------------------</p> <span>以form表单并且是post方式提交,同时还要带上csrf的token才能正常退出。</span> <form action="/logout" method="post"> <!-- 这一行代码很关键;这个name="_csrf"是默认的固定写法,除非在配置类自定义。 --> <input type="hidden" name="_csrf" th:value="${_csrf.token}" th:if="${_csrf}"> <input type="submit" value="退出"> </form> <p>------------------------------------------------------------</p> <span>以form表单并且是post方式提交,同时还要带上csrf的token才能正常访问 post 方式的资源</span> <form action="/authorityTest" method="post"> <!-- 这一行代码很关键;这个name="_csrf"是默认的固定写法,除非在配置类自定义。 --> <input type="hidden" name="_csrf" th:value="${_csrf.token}" th:if="${_csrf}"> <input type="submit" value="访问/authorityTest API"> </form> <p>------------------------------------------------------------</p> <span>以form表单并且是post方式提交,不带上csrf的token访问 post 方式的资源会报错</span> <form action="/authorityTest" method="post"> <!-- 这一行代码很关键;这个name="_csrf"是默认的固定写法,除非在配置类自定义。 --> <!-- <input type="hidden" name="_csrf" th:value="${_csrf.token}" th:if="${_csrf}"> --> <input type="submit" value="访问/authorityTest API"> </form> </body> </html>
-
在
src/main/resources/templates
目录下创建loginerror.html页面。<!DOCTYPE> <html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div>操作失败,请重新登录<a href="/showLogin">跳转</a></div> </body> </html>
-
在
src/main/resources/templates
目录下创建loginerror.html页面。<!DOCTYPE> <html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div>csrf_token:<span th:text = "${_csrf.token}"></span></div> <div>AuthorityTest.权限控制-测试</div> </body> </html>
八、基于token的前后端分离
前面的内容都是在前后端不分离的情况下使用Spring Security,现在的许多项目都是以前后端分离的方式进行开发,前面介绍的内容不太适用,下面就介绍一下基于token的前后端分离的情况下使用Spring Security
代码示例如下:
-
引入依赖
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.1.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.xxx</groupId> <artifactId>spring_security_demo_separate</artifactId> <version>0.0.1-SNAPSHOT</version> <name>spring_security_demo_separate</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <!-- security组件 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- web组件 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
-
编写数据库中用户信息表所对应的用户信息实体类SysUser
package com.xxx.springsecurity.demo.model.pojo; import java.io.Serializable; import java.util.Date; /** * 用户对象 对应着数据库中的 sys_user 表信息和一些扩展信息 */ public class SysUser implements Serializable{ private static final long serialVersionUID = 1L; /** 用户ID (主键) */ private Long userId; /** 部门ID */ //只是做个简单的登录认证例子,这个就不要了。 //private Long deptId; /** 用户账号 (用户名) */ private String userAccount; /** 密码 */ private String password; /** 用户昵称 */ private String nickName; /** 用户头像 */ private String avatar; /** 用户邮箱 */ private String email; /** 手机号码 */ private String phonenumber; /** 用户姓名 */ private String userFullName; // 改名以避免与 UserDetails 的 getUsername 冲突 /** 用户身份证号 */ private String userIdCard; /** 用户性别 */ private String sex; /** 帐号状态(0正常 1停用) */ private String status; /** 删除标志(0代表存在 2代表删除) */ private String delFlag; /** 最后登录IP */ private String loginIp; /** 最后登录时间 */ private Date loginDate; /** 创建者 */ private String createBy; /** 创建时间 */ private Date createTime; /** 更新者 */ private String updateBy; /** 更新时间 */ private Date updateTime; /** 备注 */ private String remark; //user_type 可能还需要 user_type //只是做个简单的登录认证例子,这个就不要了。 // 实际项目开发中可能需要用到的扩展信息 //只是做个简单的登录认证例子,这个就不要了。 /** 部门对象 */ //private SysDept dept; /** 角色对象 */ //private List<SysRole> roles; /** 角色组 */ //private Long[] roleIds; /** 岗位组 */ //private Long[] postIds; /** 角色ID */ //private Long roleId; // 写上get、set方法 }
-
编写UserDetails的实现类,里面主要就记录用户登录的一些相关信息。
package com.xxx.springsecurity.demo.model.businessdata; import java.util.Collection; import java.util.Set; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import com.xxx.springsecurity.demo.model.pojo.SysUser; /** * 登录用户身份权限 */ public class LoginUser implements UserDetails { private static final long serialVersionUID = 1L; /** 用户ID **/ private Long userId; // 部门ID . 只是做个简单的登录认证例子,这个就不要了。 // private Long deptId; // 用户唯一标识 private String token; // 登录时间 private Long loginTime; // 过期时间 private Long expireTime; // 登录IP地址 private String ipaddr; // 登录地点 private String loginLocation; // 浏览器类型 private String browser; // 操作系统 private String os; // 权限列表 private Set<String> permissions; // 用户信息。数据库表中对应的用户信息,和一些用户相关联的信息。 // 为了和 spring security 中的 User 类区分,这里使用 SysUser 类, private SysUser user; public Long getUserId() { return userId; } public void setUserId(Long userId) { this.userId = userId; } /* public Long getDeptId() { return deptId; } //只是做个简单的登录认证例子,这个就不要了。 public void setDeptId(Long deptId) { this.deptId = deptId; } */ public String getToken() { return token; } public void setToken(String token) { this.token = token; } public LoginUser() { } public LoginUser(SysUser user, Set<String> permissions) { this.user = user; this.permissions = permissions; } public LoginUser(Long userId, /* Long deptId, */ SysUser user, Set<String> permissions) { this.userId = userId; /* this.deptId = deptId; */ //只是做个简单的登录认证例子,这个就不要了。 this.user = user; this.permissions = permissions; } @Override public String getPassword() { return user.getPassword(); } @Override public String getUsername() { return user.getUserAccount(); } /** * 账户是否未过期,过期无法验证 */ @Override public boolean isAccountNonExpired() { return true; } /** * 指定用户是否解锁,锁定的用户无法进行身份验证 * * @return */ @Override public boolean isAccountNonLocked() { return true; } /** * 指示是否已过期的用户的凭据(密码),过期的凭据防止认证 * * @return */ @Override public boolean isCredentialsNonExpired() { return true; } /** * 是否可用 ,禁用的用户不能身份验证 * * @return */ @Override public boolean isEnabled() { return true; } public Long getLoginTime() { return loginTime; } public void setLoginTime(Long loginTime) { this.loginTime = loginTime; } public String getIpaddr() { return ipaddr; } public void setIpaddr(String ipaddr) { this.ipaddr = ipaddr; } public String getLoginLocation() { return loginLocation; } public void setLoginLocation(String loginLocation) { this.loginLocation = loginLocation; } public String getBrowser() { return browser; } public void setBrowser(String browser) { this.browser = browser; } public String getOs() { return os; } public void setOs(String os) { this.os = os; } public Long getExpireTime() { return expireTime; } public void setExpireTime(Long expireTime) { this.expireTime = expireTime; } public Set<String> getPermissions() { return permissions; } public void setPermissions(Set<String> permissions) { this.permissions = permissions; } public SysUser getUser() { return user; } public void setUser(SysUser user) { this.user = user; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return null; } }
-
编写登录所需要的参数的一个类
package com.xxx.springsecurity.demo.model.inputparameter; public class LoginParameter { // 用户名 private String username; //用户密码 private String password; //验证码;只是做个简单的登录认证例子,这个就不要了。 //private String code; //唯一标识;只是做个简单的登录认证例子,这个就不要了。 //private String uuid; public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } }
-
编写UserDetailsService的实现类
package com.xxx.springsecurity.demo.service; import java.util.HashSet; import java.util.Set; 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.password.PasswordEncoder; import org.springframework.stereotype.Service; import com.xxx.springsecurity.demo.model.businessdata.LoginUser; import com.xxx.springsecurity.demo.model.pojo.SysUser; @Service public class UserDetailServiceImpl implements UserDetailsService{ //@Autowired //private PasswordEncoder passwordEncoder; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //1、根据用户名去数据库查询 ,如果不存在就抛UsernameNotFoundException。 if(!"admin".equals(username)) {// 为了方便这里就不建数据库表了,也就不查数据库了;就假设数据库中只有 admin 这个用户 throw new UsernameNotFoundException("用户不存在"); } //2、设置一个密码,让 SpringSecurity 内部比较密码 String password = "$2a$10$ODk4SFGfaikJDhHSaxJGg.I0rmPvBUaYcxpivb9HayKtcAzU/EYge";//假设这个是数据库的密码 // 这个密码的原文就是123 也就是使用 passwordEncoder.encode("123"); 得到的密码密文。 SysUser user = new SysUser(); user.setUserAccount("admin"); user.setPassword(password); Set<String> permission = new HashSet<>(); permission.add("权限1"); permission.add("权限2"); return new LoginUser(14L, user, permission); // 密码的比较校验和权限的检验都由 Spring Security 内部的逻辑来进行 } }
-
编写缓存数据用的一个类,可以用这个类缓存用户的登录信息,比如:用户的token。
package com.xxx.springsecurity.demo.cache; public class RedisCache { // 这里只是代码示例而已,就不写Redis的具体实现代码了。用一个Map来模拟Redis。 private static final java.util.Map<String, Object> cache = new java.util.HashMap<>(); public static void put(String key, Object value) { cache.put(key,value); } public static Object get(String key) { return cache.get(key); } public static void remove(String key) { cache.remove(key); } }
-
编写一个无法访问Api资源的处理类
package com.xxx.springsecurity.demo.handle; import java.io.IOException; import java.io.PrintWriter; import java.io.Serializable; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; /** * 那些没有登录就访问Api资源,或者退出了登录又用旧的token请求访问Api资源,或者token过期等等就会来到这个类 */ @Component public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable{ private static final long serialVersionUID = -8970718410437077606L; @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e)throws IOException{ //int code = HttpStatus.UNAUTHORIZED; 其实就是401 //String msg = StringUtils.format("请求访问:{},认证失败,无法访问系统资源", request.getRequestURI()); //ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg))); response.setStatus(200); response.setContentType("application/json"); response.setCharacterEncoding("utf-8"); PrintWriter out = response.getWriter(); /** * 在使用 PrintWriter 输出数据到 HTTP 响应时,通常不需要显式地调用 out.close() 方法来关闭输出流。 * * Servlet 容器(如 Tomcat)负责管理请求和响应对象的生命周期。当请求处理完成之后,容器会自动处理资源的释放工作, * 包括关闭与 HttpServletResponse 相关联的输出流。 * * 在下面使用了 out.flush() 方法。这个方法是用来刷新缓冲区的数据,确保所有待处理的数据都被发送到客户端。 * 但即使不显式调用 flush(),在 PrintWriter 或 OutputStream 被垃圾回收时,Java 也会尝试自动刷新缓冲区。 * 然而,为了保证及时将数据发送给客户端,最好还是显式调用 flush()。 */ out.print("{\"code\":\"401\",\"msg\":\"请求访问:xx/xx,认证失败,无法访问系统资源成功\"}"); out.flush(); //out.close(); } }
-
编写一个自定义退出登录成功的处理器
package com.xxx.springsecurity.demo.handle; import java.io.IOException; import java.io.PrintWriter; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; import com.xxx.springsecurity.demo.model.businessdata.LoginUser; import com.xxx.springsecurity.demo.cache.RedisCache; /** * 自定义退出处理类 返回成功 */ @Configuration public class CustomLogoutSuccessHandler implements LogoutSuccessHandler{ //@Autowired //private TokenService tokenService; 示例代码 没有TokenService /** * 退出处理 */ @Override public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)throws IOException, ServletException{ // 这个代码有一个问题:当你没有携带token就发起退出登录请求 或者 通过token得到的用户为null,他会提示退出成功,但实际上并没有退出 //LoginUser loginUser = tokenService.getLoginUser(request);//这只是个示例 没有TokenService 所以使用下面两行代码代替 // 从请求头获取token String token = request.getHeader("token"); // LoginUser loginUser = (LoginUser)RedisCache.get(token);//这只是个示例,没有Redis,这里就模拟一下从 Redis 中获取User信息 if (loginUser != null){ //String userName = loginUser.getUsername(); 下面记录日志使用 // 删除用户缓存记录 //tokenService.delLoginUser(loginUser.getToken()); 这只是个示例 没有TokenService 所以使用下面的一行代码代替 RedisCache.remove(token); // 记录用户退出日志,如果觉得有必要就写个方法记录一下退出成功的日志。同步记录或者异步的方式记录都行 //xxx.recordLogininfor(userName, Constants.LOGOUT, "退出成功"); } response.setStatus(200); response.setContentType("application/json"); response.setCharacterEncoding("utf-8"); PrintWriter out = response.getWriter(); /** * 在使用 PrintWriter 输出数据到 HTTP 响应时,通常不需要显式地调用 out.close() 方法来关闭输出流。 * * Servlet 容器(如 Tomcat)负责管理请求和响应对象的生命周期。当请求处理完成之后,容器会自动处理资源的释放工作, * 包括关闭与 HttpServletResponse 相关联的输出流。 * * 在下面使用了 out.flush() 方法。这个方法是用来刷新缓冲区的数据,确保所有待处理的数据都被发送到客户端。 * 但即使不显式调用 flush(),在 PrintWriter 或 OutputStream 被垃圾回收时,Java 也会尝试自动刷新缓冲区。 * 然而,为了保证及时将数据发送给客户端,最好还是显式调用 flush()。 */ out.print("{\"code\":\"200\",\"msg\":\"退出成功\"}"); out.flush(); //out.close(); } }
-
编写一个访问Api资源时的Token认证过滤器;(非常重要)
package com.xxx.springsecurity.demo.filter; import java.io.IOException; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import com.xxx.springsecurity.demo.cache.RedisCache; import com.xxx.springsecurity.demo.model.businessdata.LoginUser; /** * token过滤器 验证token有效性 */ @Component public class JwtAuthenticationTokenFilter extends OncePerRequestFilter{ @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)throws ServletException, IOException{ // 从请求头获取token String token = request.getHeader("token"); // 当请求携带的token不为空的时候,就解析token获取用户信息,然后将登录的用户信息放到spring security的上下文中 if (token != null && !"".equals(token)) { LoginUser loginUser = (LoginUser)RedisCache.get(token); if (loginUser != null && SecurityContextHolder.getContext().getAuthentication() == null) { UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities()); authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authenticationToken); } } chain.doFilter(request, response); } }
-
编写跨域配置类
package com.xxx.springsecurity.demo.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.CorsFilter; @Configuration public class CorsConfig { /** * 跨域配置 */ @Bean public CorsFilter corsFilter(){ CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials(true); // 设置访问源地址 //config.addAllowedOriginPattern("*"); config.addAllowedOrigin("*");// 不成功打开上面一行代码的注释,同时删除掉当前这一行代码 // 设置访问源请求头 config.addAllowedHeader("*"); // 设置访问源请求方法 config.addAllowedMethod("*"); // 有效期 1800秒 config.setMaxAge(1800L); // 添加映射路径,拦截一切请求 UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); // 返回新的CorsFilter return new CorsFilter(source); } }
-
编写 Spring Security 配置类
package com.xxx.springsecurity.demo.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; 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.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.logout.LogoutFilter; import org.springframework.web.filter.CorsFilter; import com.xxx.springsecurity.demo.filter.JwtAuthenticationTokenFilter; import com.xxx.springsecurity.demo.handle.AuthenticationEntryPointImpl; import com.xxx.springsecurity.demo.handle.CustomLogoutSuccessHandler; @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter{ //自定义用户认证逻辑 @Autowired private UserDetailsService userDetailsService; //token认证过滤器 @Autowired private JwtAuthenticationTokenFilter authenticationTokenFilter; //自定义的访问Api资源失败处理类;那些没有登录就访问Api资源,或者退出了登录又用旧的token请求访问Api资源,或者token过期等等就会来到这个类 @Autowired private AuthenticationEntryPointImpl unauthorizedHandler; //自定义的退出登录处理类 @Autowired private CustomLogoutSuccessHandler logoutSuccessHandler; //跨域过滤器 @Autowired private CorsFilter corsFilter; /** * 解决 无法直接注入 AuthenticationManager;(登录的代码逻辑需要用到这个) * * @return * @throws Exception */ @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception{ return super.authenticationManagerBean(); } @Bean public BCryptPasswordEncoder bCryptPasswordEncoder(){ // 自定义登录逻辑的时候容器里面必须要有PasswordEncoder的实例 也就是我们不能直接new PasswordEncoder // 因为 Spring Security 内部会使用 PasswordEncoder 来和 UserDetailsService.loadUserByUsername(username)方法返回的 UserDetails 里面的密码做匹配校验 return new BCryptPasswordEncoder(); } /** * 配置这个的作用是将我们自定义的登录逻辑中关键的 UserDetailsService 配置到这个认证管理构建器中, * 这样在调用 登录Api接口 的时候就会用到我们自己自定义的UserDetailsService; * * 同时将密码比较用到的 BCryptPasswordEncoder 类也配置到这个认证管理构建器中; */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception{ auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder()); } /** * anyRequest | 匹配所有请求路径 * access | SpringEl表达式结果为true时可以访问 * anonymous | 匿名可以访问 * denyAll | 用户不能访问 * fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录) * hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问 * hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问 * hasAuthority | 如果有参数,参数表示权限,则其权限可以访问 * hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问 * hasRole | 如果有参数,参数表示角色,则其角色可以访问 * permitAll | 用户可以任意访问 * rememberMe | 允许通过remember-me登录的用户访问 * authenticated | 用户登录后可访问 */ @Override protected void configure(HttpSecurity httpSecurity) throws Exception{ // 基于token的前后端分离方式,就不需要配置formLogin()了,因为formLogin()是表单登录,前后端分离不需要这种表单登录方式 httpSecurity // CSRF禁用,因为不使用session .csrf().disable() // 配置自定义的访问Api资源失败处理类;那些没有登录就访问Api资源,或者退出了登录又用旧的token请求访问Api资源,或者token过期等等就会来到这个类 .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and() // 基于token的前后端分离方式,所以不需要session .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() // 过滤请求 .authorizeRequests() // 对于登录login 注册register 验证码captchaImage 允许匿名访问 .antMatchers("/login", "/register", "/captchaImage").anonymous() .antMatchers( HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**" ).permitAll() .antMatchers("/swagger-ui.html").anonymous() .antMatchers("/swagger-resources/**").anonymous() .antMatchers("/webjars/**").anonymous() .antMatchers("/*/api-docs").anonymous() .antMatchers("/druid/**").anonymous() // 除上面外的所有请求全部需要鉴权认证 .anyRequest().authenticated() .and() // 禁用HTTP响应头中的X-Frame-Options设置。 // 当你调用 httpSecurity.headers().frameOptions().disable() 时,你实际上是告诉Spring Security不要向响应添加 X-Frame-Options 头。 .headers().frameOptions().disable(); // 退出登录处理 httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler); // 添加JWT filter httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); // 添加CORS filter httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class); httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class); } }
-
编写一个查询数据用的Api接口(编写Controller类)
package com.xxx.springsecurity.demo.controller; import java.util.HashMap; import java.util.Map; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class TestController { @PostMapping("/test") public Map<String,String> toError() { System.out.println("ssssssa"); Map<String,String> result = new HashMap<>(); result.put("test", "vmkjsf"); result.put("ceshi", "nfdgt"); return result; } }
-
编写一个用于登录用的Api接口(在controller包下写)
package com.xxx.springsecurity.demo.controller; import java.util.HashMap; import java.util.Map; import javax.annotation.Resource; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import com.xxx.springsecurity.demo.cache.RedisCache; import com.xxx.springsecurity.demo.model.businessdata.LoginUser; import com.xxx.springsecurity.demo.model.inputparameter.LoginParameter; import org.springframework.security.core.Authentication; @RestController public class LoginController { @Resource private AuthenticationManager authenticationManager; @PostMapping("/login") public Map<String,String> login(@RequestBody LoginParameter loginParameter) { // 这个类是记录用户认证信息用的 Authentication authentication = null; try{ // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(loginParameter.getUsername(), loginParameter.getPassword())); }catch (Exception e){ // BadCredentialsException:由于凭据(密码)无效而拒绝身份验证请求时抛出。如果抛出此异常,则意味着该帐户既未被锁定也未被禁用。 if (e instanceof BadCredentialsException) { //throw new UserPasswordNotMatchException(); } else { //throw new ServiceException(e.getMessage()); } } // 判空! 一般来说 Authentication(认证管理器)为空就是认证错误失败,一般来说也就是用户名或者密码输入错误了。 //if (Objects.isNull(authentication)) { // throw new RuntimeException("用户名或密码有误"); //} LoginUser loginUser = (LoginUser) authentication.getPrincipal(); // 使用 loginUser 生成token。这是个例子,生成token 的代码逻辑就不写了,用一个自定义的字符串代替生成的 token 。 // 一般来说用 JWT 结合登录成功后得到的 loginUser 对象里面的关键信息(例如:用户名密码)生成token String token = "xxxxxxxxx-"+loginUser.getUsername()+"-xxx-"+loginUser.getPassword(); // 生成的token 应该保存在缓存中,缓存可以使用Redis 或者其他缓存工具。放入缓存既可以方便登录验证的用户数据的获取,也可以方便用户退出登录(清理掉缓存的数据就是退出登录了) RedisCache.put(token, loginUser); Map<String,String> result = new HashMap<>(); // 实际开发中可以不用Map,可以自定义返回对象结果。这里只是一个例子,示例。 result.put("token", token); return result; } }
登录的写法说明介绍: