Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。它是用于保护基于Spring的应用程序的实际标准。Spring Security是一个框架,致力于为Java应用程序提供身份验证和授 权。与所有Spring项目一样,Spring Security的真正强大之处在于可以轻松扩展以满足自定义要求.
功能简介
-
认证: 用户登录, 解决的是"你是谁?"
-
授权: 判断用户拥有什么权限,可以访问什么资源. 解决的是"你能干什么?"
-
安全防护,防止跨站请求,session 攻击等
SpringSecurity依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
测试案例
使用Spring Initializr 快速过构建Spring Boot Demo,Spring Boot版本选择2.6.8 , 并选中Spring Web 模块,编写controller如下
public class HelloSecurityController {
@RequestMapping("/hello")
public String hello() {
return "hello security";
}
}
访问 http://localhost:18080/hello
Spring Boot 已经为 Spring Security 提供了默认配置,默认所有资源都必须认证通过才能访问。那么问题来了!此刻并没有连接数据库,也并未在内存中指定认证用户,如何认证呢?
其实 Spring Boot 已经提供了默认用户名 user,密码在项目启动时随机生成,如图:
认证通过后可以继续访问处理器资源.
Spring Security 认证
网上找了一套半成品代码(有需要的可找我提供),我们用Spring Security来完善一下;
技术支持:SpringBoot 2.6.8,thymeleaf,maven
pom依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</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-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!--添加lombok 依赖 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--添加mp依赖 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.2</version>
</dependency>
<!--添加mysql 依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.21</version>
</dependency>
<!--添加redis 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
结构层次也很简单;
SpringSecurity认证基本原理与认证2种方式
在使用 Spring Security 框架,该框架会默认自动地替我们将系统中的资源进行保护,每次访问资源的时候都必须经过一层身份的校验,如果通过了则重定向到我们输入的url中,否则访问是要被拒绝的。那 么 Spring Security 框架是如何实现的呢? Spring Security功能的实现主要是由一系列过滤器相互配合完成。也称之为过滤器链,如图:
过滤器是一种典型的AOP思想,下面简单了解下这些过滤器链,后续再源码剖析中在涉及到过滤器链在仔细讲解.
-
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter
根据请求封装获取WebAsyncManager,从WebAsyncManager获取/注册的安全上下文可调 用处理拦截器
-
org.springframework.security.web.context.SecurityContextPersistenceFilter
SecurityContextPersistenceFilter主要是使用SecurityContextRepository在session中保存 或更新一个SecurityContext,并将SecurityContext给以后的过滤器使用,来为后续fifilter 建立所需的上下文。SecurityContext中存储了当前用户的认证以及权限信息。
-
org.springframework.security.web.header.HeaderWriterFilter
向请求的Header中添加相应的信息,可在http标签内部使用security:headers来控制
-
org.springframework.security.web.csrf.CsrfFilter
csrf又称跨域请求伪造,SpringSecurity会对所有post请求验证是否包含系统生成的csrf的 token信息,如果不包含,则报错。起到防止csrf攻击的效果
-
org.springframework.security.web.authentication.logout.LogoutFilter
匹配URL为/logout的请求,实现用户退出,清除认证信息
-
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
表单认证操作全靠这个过滤器,默认匹配URL为/login且必须为POST请求
-
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter
如果没有在配置文件中指定认证页面,则由该过滤器生成一个默认认证页面
-
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter
由此过滤器可以生产一个默认的退出登录页面
-
org.springframework.security.web.authentication.www.BasicAuthenticationFilter
此过滤器会自动解析HTTP请求中头部名字为Authentication,且以Basic开头的头信息
-
org.springframework.security.web.savedrequest.RequestCacheAwareFilter
通过HttpSessionRequestCache内部维护了一个RequestCache,用于缓存 HttpServletRequest
-
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
针对ServletRequest进行了一次包装,使得request具有更加丰富的API
-
org.springframework.security.web.authentication.AnonymousAuthenticationFilter
当SecurityContextHolder中认证信息为空,则会创建一个匿名用户存入到 SecurityContextHolder中。spring security为了兼容未登录的访问,也走了一套认证流程, 只不过是一个匿名的身份
-
org.springframework.security.web.session.SessionManagementFilter
securityContextRepository限制同一用户开启多个会话的数量
-
org.springframework.security.web.access.ExceptionTranslationFilter
异常转换过滤器位于整个springSecurityFilterChain的后方,用来转换整个链路中出现的异 常
-
org.springframework.security.web.access.intercept.FilterSecurityIntercepto
获取所配置资源访问的授权信息,根据SecurityContextHolder中存储的用户信息来决定其 是否有权限
Spring Security默认加载15个过滤器, 但是随着配置可以增加或者删除一些过滤器.
认证方式
认证方式有两种,一种是HttpBasic认证,另外一种是formLogin登录认证模式
HttpBasic认证
HttpBasic登录验证模式是Spring Security实现登录验证最简单的一种方式,也可以说是最简陋的一种方式。它的目的并不是保障登录验证的绝对安全,而是提供一种“防君子不防小人”的登录验证;HttpBasic模式要求传输的用户名密码使用Base64模式进行加密。如果用户名是 "admin" , 密码是“ admin”,则将字符串"admin:admin" 使用Base64编码算法加密。加密结果可能是: YWtaW46YWRtaW4=。HttpBasic模式真的是非常简单又简陋的验证模式,Base64的加密算法是可逆的,想要破解并不难.
formLogin登录认证模式
通过携带Http的Header进行 简单的登录验证,而且没有定制的登录页面,所以使用场景比较窄。对于一个完整的应用系统,与 登录验证相关的页面都是高度定制化的,非常美观而且提供多种登录方式。这就需要Spring Security支持我们自己定制登录页面, spring boot2.0以上版本(依赖Security 5.X版本)默认会生 成一个登录页面.
表单认证
下面我们根据半成品代码自定义一个表单认证(由于HttpBasic认证不安全,所以不予深究...)
在config包下编写SecurityConfiguration配置类
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* http请求方法
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
/* http.httpBasic() //开启httpBasic认证
.and().authorizeRequests().anyRequest().authenticated(); //所有请求都需要认证之后访问*/
http.formLogin().loginPage("/login.html")//开启表单认证
.and().authorizeRequests() //放行登录页面
.anyRequest().authenticated();
}
注意:WebSecurityConfigurerAdapter 抽象类有三个configure方法,分别对应的是 身份安全管理器, 忽略静态资源, http请求方法;
点击运行发现如下问题
因为设置登录页面为login.html 后面配置的.anyRequest().authenticated()是所有请求都登录认证,陷入了死循环. 所以需要将 login.html放行不需要登录认证
这边修改一下代码
@Override
protected void configure(HttpSecurity http) throws Exception {
/* http.httpBasic() //开启httpBasic认证
.and().authorizeRequests().anyRequest().authenticated(); //所有请求都需要认证之后访问*/
http.formLogin().loginPage("/login.html") //开启表单认证
.and().authorizeRequests().antMatchers("/login.html").permitAll() //放行登录页面
.anyRequest().authenticated();
}
点击重新测试如下
提示404了。这是怎么回事?
该工程页面已经整合了 thymeleaf ,那我们直接访问login.html就不行了,他需要请求之后然后跳转到该页面,那我们就写个方法让他跳转到该页面呗;
@RequestMapping("/toLoginPage")
public String toLoginPage() {
return "login";
}
然后我们再修改一下SecurityConfig
@Override
protected void configure(HttpSecurity http) throws Exception {
/* http.httpBasic() //开启httpBasic认证
.and().authorizeRequests().anyRequest().authenticated(); //所有请求都需要认证之后访问*/
http.formLogin().loginPage("/toLoginPage") //开启表单认证
.and().authorizeRequests().antMatchers("/toLoginPage").permitAll() //放行登录页面
.anyRequest().authenticated();
}
重新启动运行,结果如下
登录是正常了,只不过样式没有了,估计是被security拦截掉了,打开控制台显示css,images,js都302了;
SecurityConfig 继承 WebSecurityConfigurerAdapter后,有三个 configure 方法,其中有一个是 configure(WebSecurity web),目测是web相关的东西,如下
@Override
public void configure(WebSecurity web) throws Exception {
//解决静态资源被拦截的问题
web.ignoring().antMatchers("/css/**","/images/**","/js/**");
}
忽略掉和静态资源相关的东西,继续访问如下:
霍,没问题了!
Spring Security 中,安全构建器 HttpSecurity 和 WebSecurity 的区别是 :
-
WebSecurity 不仅通过 HttpSecurity 定义某些请求的安全控制,也通过其他方式定义其他某些 请求可以忽略安全控制;
-
HttpSecurity 仅用于定义需要安全控制的请求(当然 HttpSecurity 也可以指定某些请求不需要 安全控制);
-
可以认为 HttpSecurity 是 WebSecurity 的一部分, WebSecurity 是包含 HttpSecurity 的更大 的一个概念;
-
构建目标不同
-
WebSecurity 构建目标是整个 Spring Security 安全过滤器 FilterChainProxy
-
HttpSecurity 的构建目标仅仅是 FilterChainProxy 中的一个 SecurityFilterChain
-
表单登录
页面问题解决了,下面我们来解决一下表单登陆的问题,上面15个过滤器链有个UsernamePasswordAuthenticationFilter是处理表单登录的;打开源码看一下都有些啥子。
在源码中可以观察到, 表单中的input的name值是 username 和 password, 并且表单提交的路径 为 /login , 表单提交方式method为 post ,
这些可以修改为自定义的值.
修改一下config配置类以及login页面
@Override
protected void configure(HttpSecurity http) throws Exception {
/* http.httpBasic() //开启httpBasic认证
.and().authorizeRequests().anyRequest().authenticated(); //所有请求都需要认证之后访问*/
http.formLogin() //开启表单认证
.loginPage("/toLoginPage") // 自定义登陆页面
.loginProcessingUrl("/login") //表单提交路径
.usernameParameter("username").passwordParameter("password") //自定义input额name值和password
.successForwardUrl("/") //登录成功之后跳转的路径
.and().authorizeRequests().antMatchers("/toLoginPage").permitAll() //放行登录页面
.anyRequest().authenticated()
.and().csrf().disable();//关闭csrf防护
}
login.html
代码修改后重启完成登录
这时候又出现了新的坑,
index.html
发现行内框架 iframe 这里出现问题了. Spring Security下,X-Frame-Options默认为DENY,非Spring Security环境下,X-Frame-Options的默认大多也是DENY,这种情况下,浏览器拒绝当前页面加载任何 Frame页面,设置含义如下:
-
DENY:浏览器拒绝当前页面加载任何Frame页面 此选择是默认的.
-
SAMEORIGIN:frame页面的地址只能为同源域名下的页面
允许iframe加载
@Override
protected void configure(HttpSecurity http) throws Exception {
/* http.httpBasic() //开启httpBasic认证
.and().authorizeRequests().anyRequest().authenticated(); //所有请求都需要认证之后访问*/
http.formLogin() //开启表单认证
.loginPage("/toLoginPage") // 自定义登陆页面
.loginProcessingUrl("/login") //表单提交路径
.usernameParameter("username").passwordParameter("password") //自定义input额name值和password
.successForwardUrl("/") //登录成功之后跳转的路径
.and().authorizeRequests().antMatchers("/toLoginPage").permitAll() //放行登录页面
.anyRequest().authenticated()
.and().headers().frameOptions().sameOrigin() //加载同源域名下iframe页面
.and().csrf().disable();//关闭csrf防护
}
修改之后再次登录如下:
完成!
基于数据库实现认证功能
之前我们所使用的用户名和密码是来源于框架自动生成的, 现在我们需要实现基于数据库中的用户名和密码功能,首先得需要实现security的一个UserDetailsService 接口, 重写这个接口里面 loadUserByUsername;
@Service
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UserService userService;
/**
* 根据用户名查询用户
* @param username 前端传入的用户名
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userService.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("user is ==>" + username);
}
Collection<? extends GrantedAuthority> authorities = new ArrayList<>();
UserDetails userDetails = new org.springframework.security.core.userdetails.User(username,
"{noop}"+user.getPassword(),//不使用密码加密
true, //用户是否启用 (true:启用)
true,//用户是否过期 (true:没有过期)
true, //用户凭证是否过期 (true:没有过期)
true, // 用户是否锁定 (true:没有锁定)
authorities);
return userDetails;
}
}
在SecurityConfiguration配置类中指定自定义用户认证
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyUserDetailsService myUserDetailsService;
/**
*身份安全管理器
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailsService);
}
}
此时,控制台也不会生成随机密码,现在走的是数据库的认证方式;
密码加密认证
在基于数据库完成用户登录的过程中,我们所是使用的密码是明文的,规则是通过对密码明文添加 {noop} 前缀。那么下面 Spring Security 中的密码编码进行一些探讨。
Spring Security 中 PasswordEncoder 就是我们对密码进行编码的工具接口。该接口只有两个功能: 一个是匹配验证(matches)。另一个是密码加密(encode)。
Spring Security主流用法就是 BCryptPasswordEncoder
BCrypt算法介绍:
任何应用考虑到安全,绝不能明文的方式保存密码。密码应该通过哈希算法进行加密。 有很多标准的算法比如SHA或者MD5,结合salt(盐)是一个不错的选择。Spring Security 提供了 BCryptPasswordEncoder类,实现Spring的PasswordEncoder接口使用BCrypt强哈希方法来加密密码。BCrypt强哈希方法 每次加密的结果都不一样,所以更加的安全。
bcrypt加密后的字符串形如:$2a$10$wouq9P/HNgvYj2jKtUN8rOJJNRVCWvn1XoWy55N3sCkEHZPo3lyWq
其中$是分割符,无意义;2a是bcrypt加密版本号;10是const的值;而后的前22位是salt值;再然后的字符串就是密码的密文了;这里的const值即生成salt的迭代次数,默认值是10,推荐值 12。
下面在项目中使用BCrypt算法进行加密
首先先看一个 PasswordEncoderFactories,它是密码器工厂
public static PasswordEncoder createDelegatingPasswordEncoder() {
String encodingId = "bcrypt";
Map<String, PasswordEncoder> encoders = new HashMap<>();
encoders.put(encodingId, new BCryptPasswordEncoder());
encoders.put("ldap", new org.springframework.security.crypto.password.LdapShaPasswordEncoder());
encoders.put("MD4", new org.springframework.security.crypto.password.Md4PasswordEncoder());
encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));
encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("SHA-1", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1"));
encoders.put("SHA-256",
new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256"));
encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder());
encoders.put("argon2", new Argon2PasswordEncoder());
return new DelegatingPasswordEncoder(encodingId, encoders);
}
之前我们在项目中密码使用的是明文的是 noop , 代表不加密使用明文密码, 现在用BCrypt只需要将 noop 换成 bcrypt 即可,如下
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userService.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("user is ==>" + username);
}
Collection<? extends GrantedAuthority> authorities = new ArrayList<>();
UserDetails userDetails = new org.springframework.security.core.userdetails.User(username,
"{bcrypt}"+user.getPassword(),//使用密码加密
true, //用户是否启用 (true:启用)
true,//用户是否过期 (true:没有过期)
true, //用户凭证是否过期 (true:没有过期)
true, // 用户是否锁定 (true:没有锁定)
authorities);
return userDetails;
}
我们可以写个main方法测试一下
public static void main(String[] args) {
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String encode = bCryptPasswordEncoder.encode("123456");
System.out.println(encode);
String encode1 = bCryptPasswordEncoder.encode("123456");
System.out.println(encode1);
}
测试结果
$2a$10$b42Oiv0L4spIBVA5NBI9U.o7DyLFfKCU46cvzdvYo8g/MTo6lwnwi
$2a$10$sMsUp6McNkscco2yTGwWOOpnL1Jgd0sj2nxU.3aaF6kHV0jUARQDq
Process finished with exit code 0
由于BCrypt是强哈希算法,可见加密结果是不一样的;
获取当前登录用户
传统web系统中,我们将登陆成功的用户放入session中,在需要的时候,可以从session中获取用户,那么spring Security中我们可以从以下两个类获得当前已经登录的用户
-
SecurityContextHolder
保留系统当前的安全上下文 SecurityContext,其中就包括当前使用系统的用户信息
-
SecurityContext
安全上下文,获取当前经过身份验证的主体或身份验证请求令牌
代码实现如下(security获取当前登录用户的三种方式):
/**
* 获取当前登录用户
* @return
*/
@GetMapping("/loginUser1")
public UserDetails getCurrnetUser1(Authentication authentication){
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
return userDetails;
}
/**
* 获取当前登录用户
* @return
*/
@GetMapping("/loginUser2")
public UserDetails getCurrnetUser2(){
UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return userDetails;
}
/**
* 获取当前登录用户
* @return
*/
@GetMapping("/loginUser3")
public UserDetails getCurrnetUser3(@AuthenticationPrincipal UserDetails userDetails){
return userDetails;
}
登录之后直接访问接口,测试结果如下
remember me功能
在大多数网站中,都会实现 Remember Me 这个功能,方便用户在下一次登录时直接登录,避免再次输入用户名以及密码去登录,Spring Security针对这个功能已经帮助我们实现,如下图:
Token=MD5(username+分隔符+expiryTime+分隔符+password)
注意: 这种方式不推荐使用, 有严重的安全问题. 就是密码信息在前端浏览器cookie中存放. 如果cookie 被盗取很容易破解.
代码实现:
protected void configure(HttpSecurity http) throws Exception {
/* http.httpBasic() //开启httpBasic认证
.and().authorizeRequests().anyRequest().authenticated(); //所有请求都需要认证之后访问*/
http.formLogin() //开启表单认证
.loginPage("/toLoginPage") // 自定义登陆页面
.loginProcessingUrl("/login") //表单提交路径
.usernameParameter("username").passwordParameter("password") //自定义input额name值和password
.successForwardUrl("/") //登录成功之后跳转的路径
.and().authorizeRequests().antMatchers("/toLoginPage").permitAll() //放行登录页面
.anyRequest().authenticated()
.and().rememberMe() //开启记住我功能
.tokenValiditySeconds(1209600) //token失效时间,默认失效时间是两周
.rememberMeParameter("remember-me") // 自定义表单name值
.and().headers().frameOptions().sameOrigin() //加载同源域名下iframe页面
.and().csrf().disable();//关闭csrf防护
}
login.html
测试如下
上面所说,密码信息也会存放在cookie中,容易被抓取破解,下面我们可以改造一下,基于持久化的Token生成方法,如下图
存入数据库Token包含:
-
token: 随机生成策略,每次访问都会重新生成
-
series: 登录序列号,随机生成策略。用户输入用户名和密码登录时,该值重新生成。使用 remember-me功能,该值保持不变
-
expiryTime: token过期时间
CookieValue=encode(series+token)
代码实现
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyUserDetailsService myUserDetailsService;
/**
*身份安全管理器
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailsService);
}
/**
* 忽略静态资源
* @param web
* @throws Exception
*/
@Override
public void configure(WebSecurity web) throws Exception {
//解决静态资源被拦截的问题
web.ignoring().antMatchers("/css/**","/images/**","/js/**");
}
/**
* http请求方法
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
/* http.httpBasic() //开启httpBasic认证
.and().authorizeRequests().anyRequest().authenticated(); //所有请求都需要认证之后访问*/
http.formLogin() //开启表单认证
.loginPage("/toLoginPage") // 自定义登陆页面
.loginProcessingUrl("/login") //表单提交路径
.usernameParameter("username").passwordParameter("password") //自定义input额name值和password
.successForwardUrl("/") //登录成功之后跳转的路径
.and().authorizeRequests().antMatchers("/toLoginPage").permitAll() //放行登录页面
.anyRequest().authenticated()
.and().rememberMe() //开启记住我功能
.tokenValiditySeconds(1209600) //token失效时间,默认失效时间是两周
.rememberMeParameter("remember-me") // 自定义表单name值
.tokenRepository(getPersistentTokenRepository()) //设置PersistentTokenRepository
.and().headers().frameOptions().sameOrigin() //加载同源域名下iframe页面
.and().csrf().disable();//关闭csrf防护
}
@Autowired
DataSource dataSource;
/**
* 负责token与数据库之间的操作
* @return
*/
@Bean
public PersistentTokenRepository getPersistentTokenRepository(){
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource); //设置数据源
tokenRepository.setCreateTableOnStartup(true); //启动时帮助我们自动创建一张表,第一次启动设置为true,第二次启动程序的时候设置false或者注释掉;
return tokenRepository;
}
}
运行后,数据库会生成一张对应的表
登陆成功之后,观察数据库,会插入一条记录.说明持久化token方式已经生效;
cookie窃取伪造登录成功,访问接口获取数据,只需在关键接口处添加如下代码,可解决此问题。如下:
@GetMapping("/{id}")
public User getById(@PathVariable Integer id) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
//如果返回true,代表这个登录认证的信息来源于自动登录
if(RememberMeAuthenticationToken.class.isAssignableFrom(authentication.getClass())){
throw new RememberMeAuthenticationException("认证来源于remember me");
}
User user = userService.getById(id);
return user;
}
自定义登录成功处理和失败处理
在某些场景下,用户登录成功或者数失败的情况下用户需要执行一些后续操作,比如登录日志搜集,或者在现在目前前后端分离的情况下,用户登录成功和失败后需要给前台页面返回对应的错误信息,有前台主导成功或者失败的页面跳转,这个时候需要用到 AuthenticationSucessHandle 与 AnthenticationFailureHandle;
默认的成功和失败处理逻辑源自于下图:
进入源码查看
-
sucess
-
faile
他们分别实现了 AuthenticationSucessHandle 接口和 AnthenticationFailureHandle 接口
自定义成功处理
实现 AuthenticationSucessHandle 接口,并重写 onAnthenticationSucess()方法;
自定义失败处理
实现 AnthenticationFailureHandle 接口,并重写 onAnthenticationFailure() 方法
代码实现:
@Service
public class MyAuthenticationService implements AuthenticationSuccessHandler,AuthenticationFailureHandler {
RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
/**
* 登录成功后的处理逻辑
* @param request
* @param response
* @param authentication
* @throws IOException
* @throws ServletException
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
System.out.println("登录成功后继续处理。。。。。。。。");
//重定向到index。html
redirectStrategy.sendRedirect(request,response,"/");
}
/**
* 登录失败后的处理逻辑
* @param request
* @param response
* @param exception
* @throws IOException
* @throws ServletException
*/
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
System.out.println("登录失败后继续处理。。。。。。。。");
redirectStrategy.sendRedirect(request,response,"/toLoginPage");
}
}
@Autowired
private MyAuthenticationService myAuthenticationService;
@Override
protected void configure(HttpSecurity http) throws Exception {
/* http.httpBasic() //开启httpBasic认证
.and().authorizeRequests().anyRequest().authenticated(); //所有请求都需要认证之后访问*/
http.formLogin() //开启表单认证
.loginPage("/toLoginPage") // 自定义登陆页面
.loginProcessingUrl("/login") //表单提交路径
.usernameParameter("username").passwordParameter("password") //自定义input额name值和password
.successForwardUrl("/") //登录成功之后跳转的路径
.successHandler(myAuthenticationService) // 登录成功处理
.failureHandler(myAuthenticationService) //登录失败处理
.and().authorizeRequests().antMatchers("/toLoginPage").permitAll() //放行登录页面
.anyRequest().authenticated()
.and().rememberMe() //开启记住我功能
.tokenValiditySeconds(1209600) //token失效时间,默认失效时间是两周
.rememberMeParameter("remember-me") // 自定义表单name值
.tokenRepository(getPersistentTokenRepository()) //设置PersistentTokenRepository
.and().headers().frameOptions().sameOrigin() //加载同源域名下iframe页面
.and().csrf().disable();//关闭csrf防护
}
测试结果(失败和成功分别演示一次)
退出登录
org.springframework.security.web.authentication.logout.LogoutFilter
匹配URL为/logout的请求,实现用户退出,清除认证信息。
只需要发送请求,请求路径为/logout即可, 当然这个路径也可以自行在配置类中自行指定, 同时退出 操作也有对应的自定义处理LogoutSuccessHandler,退出登录成功后执行,退出的同时如果有 remember-me的数据,同时一并删除
看一下 LogoutFilter 源码:
public class LogoutFilter extends GenericFilterBean {
private RequestMatcher logoutRequestMatcher;
private final LogoutHandler handler;
private final LogoutSuccessHandler logoutSuccessHandler;
/**
* Constructor which takes a <tt>LogoutSuccessHandler</tt> instance to determine the
* target destination after logging out. The list of <tt>LogoutHandler</tt>s are
* intended to perform the actual logout functionality (such as clearing the security
* context, invalidating the session, etc.).
*/
public LogoutFilter(LogoutSuccessHandler logoutSuccessHandler, LogoutHandler... handlers) {
this.handler = new CompositeLogoutHandler(handlers);
Assert.notNull(logoutSuccessHandler, "logoutSuccessHandler cannot be null");
this.logoutSuccessHandler = logoutSuccessHandler;
setFilterProcessesUrl("/logout");
}
public LogoutFilter(String logoutSuccessUrl, LogoutHandler... handlers) {
this.handler = new CompositeLogoutHandler(handlers);
Assert.isTrue(!StringUtils.hasLength(logoutSuccessUrl) || UrlUtils.isValidRedirectUrl(logoutSuccessUrl),
() -> logoutSuccessUrl + " isn't a valid redirect URL");
SimpleUrlLogoutSuccessHandler urlLogoutSuccessHandler = new SimpleUrlLogoutSuccessHandler();
if (StringUtils.hasText(logoutSuccessUrl)) {
urlLogoutSuccessHandler.setDefaultTargetUrl(logoutSuccessUrl);
}
this.logoutSuccessHandler = urlLogoutSuccessHandler;
setFilterProcessesUrl("/logout");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
}
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (requiresLogout(request, response)) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Logging out [%s]", auth));
}
this.handler.logout(request, response, auth);
this.logoutSuccessHandler.onLogoutSuccess(request, response, auth);
return;
}
chain.doFilter(request, response);
}
/**
* Allow subclasses to modify when a logout should take place.
* @param request the request
* @param response the response
* @return <code>true</code> if logout should occur, <code>false</code> otherwise
*/
protected boolean requiresLogout(HttpServletRequest request, HttpServletResponse response) {
if (this.logoutRequestMatcher.matches(request)) {
return true;
}
if (this.logger.isTraceEnabled()) {
this.logger.trace(LogMessage.format("Did not match request to %s", this.logoutRequestMatcher));
}
return false;
}
public void setLogoutRequestMatcher(RequestMatcher logoutRequestMatcher) {
Assert.notNull(logoutRequestMatcher, "logoutRequestMatcher cannot be null");
this.logoutRequestMatcher = logoutRequestMatcher;
}
public void setFilterProcessesUrl(String filterProcessesUrl) {
this.logoutRequestMatcher = new AntPathRequestMatcher(filterProcessesUrl);
}
}
默认退出路径 setFilterProcessesUrl("/logout");
发送 "/logout" 之后,退出完成之后没有指定的情况下会默认跳转到登录页面,如下源码:
代码实现如下:
index.html
<div class="header bg-main">
<div class="logo margin-big-left fadein-top">
<h1>后台管理中心</h1>
</div>
<div class="head-l">
<a class="button button-little bg-red" href="/logout">
<span class="icon-power-off"></span>退出登录</a></div>
</div>
后端代码 SecurityConfig:
@Override
protected void configure(HttpSecurity http) throws Exception {
/* http.httpBasic() //开启httpBasic认证
.and().authorizeRequests().anyRequest().authenticated(); //所有请求都需要认证之后访问*/
http.formLogin() //开启表单认证
.loginPage("/toLoginPage") // 自定义登陆页面
.loginProcessingUrl("/login") //表单提交路径
.usernameParameter("username").passwordParameter("password") //自定义input额name值和password
.successForwardUrl("/") //登录成功之后跳转的路径
.successHandler(myAuthenticationService) // 登录成功处理
.failureHandler(myAuthenticationService) //登录失败处理
.and().logout().logoutUrl("/logout") //退出
.logoutSuccessHandler(myAuthenticationService) //退出后处理
.and().authorizeRequests().antMatchers("/toLoginPage").permitAll() //放行登录页面
.anyRequest().authenticated()
.and().rememberMe() //开启记住我功能
.tokenValiditySeconds(1209600) //token失效时间,默认失效时间是两周
.rememberMeParameter("remember-me") // 自定义表单name值
.tokenRepository(getPersistentTokenRepository()) //设置PersistentTokenRepository
.and().headers().frameOptions().sameOrigin() //加载同源域名下iframe页面
.and().csrf().disable();//关闭csrf防护
}
@Service
public class MyAuthenticationService implements AuthenticationSuccessHandler,AuthenticationFailureHandler, LogoutSuccessHandler {
RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@Autowired
ObjectMapper objectMapper;
/**
* 登录成功后的处理逻辑
* @param request
* @param response
* @param authentication
* @throws IOException
* @throws ServletException
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
System.out.println("登录成功后继续处理。。。。。。。。");
//重定向到index。html
redirectStrategy.sendRedirect(request,response,"/");
/*
Map result = new HashMap();
result.put("code", HttpStatus.OK.value()); //200
result.put("message","登录成功");
response.setContentType("application/json;charset=UTF-8");
//响应
response.getWriter().write(objectMapper.writeValueAsString(result));
*/
}
/**
* 登录失败后的处理逻辑
* @param request
* @param response
* @param exception
* @throws IOException
* @throws ServletException
*/
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
System.out.println("登录失败后继续处理。。。。。。。。");
redirectStrategy.sendRedirect(request,response,"/toLoginPage");
/* Map result = new HashMap();
result.put("code", HttpStatus.UNAUTHORIZED.value()); //401
result.put("message","登录失败");
response.setContentType("application/json;charset=UTF-8");
//响应
response.getWriter().write(objectMapper.writeValueAsString(result));*/
}
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
System.out.println("退出之后继续处理。。。。");
redirectStrategy.sendRedirect(request,response,"/toLoginPage");
}
}
测试结果