在config包下编写SecurityConfiguration配置类
最重要的是继承WebSecurityConfigurerAdapter这个抽象类,然后实现里面的方法
package com.lagou.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
* Security配置类
*/
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
/**
* http请求处理方法
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
/* http.httpBasic() // 开启httpBasic认证
.and().authorizeRequests().anyRequest().authenticated(); // 所有请求都需要认证后才能访问 */
http.formLogin()// 开启表单认证
.and().authorizeRequests()
.anyRequest().authenticated();// 所有请求都需要登录认证才能访问;
}
}
因为设置登录页面为login.html 后面配置的是所有请求都登录认证,陷入了死循环. 所以需要将login.html放行不需要登录认证
package com.lagou.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
* Security配置类
*/
@Configuration
public class SecurityConfiguration 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()
.antMatchers("/login.html").permitAll()// 放行登录页面
.anyRequest().authenticated();// 所有请求都需要登录认证才能访问;
}
}
spring boot整合thymeleaf 之后 所有的静态页面以放在resources/templates下面,所以得通过请求访问到模板页面, 将/login.html修改为/toLoginPage
package com.lagou.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
* Security配置类
*/
@Configuration
public class SecurityConfiguration 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("/toLoginPage") // 设置自定义的登录页面
.and().authorizeRequests()
.antMatchers("/toLoginPage").permitAll()// 放行登录页面
.anyRequest().authenticated();// 所有请求都需要登录认证才能访问;
}
}
使用这种方式访问LoginController 然后返回登录页面
package com.lagou.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* 处理登录业务
*/
@Controller
public class LoginController {
/**
* 跳转登录页面
*
* @return
*/
@RequestMapping("/toLoginPage")
public String toLoginPage() {
return "login";
}
}
因为访问login.html需要一些js , css , image等静态资源信息, 所以需要将静态资源放行, 不需要认证
package com.lagou.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
* Security配置类
*/
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
// 这个重写方法用来静态资源放行
@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") // 设置自定义的登录页面
.and().authorizeRequests()
.antMatchers("/toLoginPage").permitAll()// 放行登录页面
.anyRequest().authenticated();// 所有请求都需要登录认证才能访问;
}
}
Spring Security 中,安全构建器HttpSecurity 和WebSecurity 的区别是 :
1. WebSecurity 不仅通过HttpSecurity 定义某些请求的安全控制,也通过其他方式定义其他某些请求可以忽略安全控制;
2. HttpSecurity 仅用于定义需要安全控制的请求(当然HttpSecurity 也可以指定某些请求不需要安全控制);
3. 可以认为HttpSecurity 是WebSecurity 的一部分, WebSecurity 是包含HttpSecurity 的更大的一个概念;
4. 构建目标不同
WebSecurity 构建目标是整个Spring Security 安全过滤器FilterChainProxy,HttpSecurity 的构建目标仅仅是FilterChainProxy 中的一个SecurityFilterChain
通过讲解过滤器链中我们知道有个过滤器UsernamePasswordAuthenticationFilter是处理表单登录的. 那么下面我们来通过源码观察下这个过滤器.
在源码中可以观察到, 表单中的input的name值是username和password, 并且表单提交的路径为/login , 表单提交方式method为post , 这些可以修改为自定义的值.
package com.lagou.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
* Security配置类
*/
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
// 这个重写方法用来静态资源放行
@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") // 修改自定义表单name值
.successForwardUrl("/")// 登录成功后跳转路径
.and().authorizeRequests()
.antMatchers("/toLoginPage").permitAll()// 放行登录页面
.anyRequest().authenticated();// 所有请求都需要登录认证才能访问;
// 关闭csrf防护
http.csrf().disable();
}
}
页面代码:
这里面的action、method和input表单中的name属性与上述,java代码中的值一一对应。这样SpringSecurity才能处理表单。
代码修改后重启完成登录:
这个时候又出现新的问题了. 这个是什么原因呢? 我们来看出现问题的具体是哪里?
发现行内框架iframe这里出现问题了. Spring Security下,X-Frame-Options默认为DENY,非SpringSecurity环境下,X-Frame-Options的默认大多也是DENY,这种情况下,浏览器拒绝当前页面加载任何Frame页面,设置含义如下:
http.formLogin()// 开启表单认证
.loginPage("/toLoginPage")// 自定义登录页面
.loginProcessingUrl("/login")// 表单提交的路径
.usernameParameter("username").passwordParameter("password") // 修改自定义表单name值
.successForwardUrl("/")// 登录成功后跳转路径
.and().authorizeRequests()
.antMatchers("/toLoginPage").permitAll()// 放行登录页面
.anyRequest().authenticated();// 所有请求都需要登录认证才能访问;
// 关闭csrf防护
http.csrf().disable();
// 允许iframe加载页面
http.headers().frameOptions().sameOrigin();
之前我们所使用的用户名和密码是来源于框架自动生成的, 那么我们如何实现基于数据库中的用户名和密码功能呢? 要实现这个得需要实现security的一个UserDetailsService接口, 重写这个接口里面loadUserByUsername即可
package com.lagou.service.impl;
import com.lagou.domain.User;
import com.lagou.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collection;
/**
* 基于数据库完成认证
*/
@Service
public class MyUserDetailsService implements UserDetailsService {
@Autowired
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("用户名没有找到:" + username);
}
Collection extends GrantedAuthority> authorities = new ArrayList<>(); // 权限的集合,先声明一个权限集合, 因为构造方法里面不能传入null
UserDetails userDetails = new org.springframework.security.core.userdetails.User
(username, "{bcrypt}" + user.getPassword(), // noop表示不使用密码加密,bcrypt表示使用bcrypt作为加密算法
true, // 用户是否启用
true, // 用户是否过期
true, // 用户凭证是否过期
true, // 用户是否锁定
authorities);
return userDetails;
}
// 这个是我测试BCrypt加密方法
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);
}
}
package com.lagou.config;
import com.lagou.service.impl.MyUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
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.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
* Security配置类
*/
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
MyUserDetailsService myUserDetailsService; // 将由spring管理的bean注入到该类中
/**
* 身份安全管理器
*
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailsService); // 使用自定义用户认证
}
// 这个重写方法用来静态资源放行
@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") // 修改自定义表单name值
.successForwardUrl("/")// 登录成功后跳转路径
.and().authorizeRequests()
.antMatchers("/toLoginPage").permitAll()// 放行登录页面
.anyRequest().authenticated();// 所有请求都需要登录认证才能访问;
// 关闭csrf防护
http.csrf().disable();
// 允许iframe加载页面
http.headers().frameOptions().sameOrigin();
}
}
在基于数据库完成用户登录的过程中,我们所是使用的密码是明文的,规则是通过对密码明文添加{noop} 前缀。那么下面 Spring Security 中的密码编码进行一些探讨。
Spring Security 中PasswordEncoder 就是我们对密码进行编码的工具接口。该接口只有两个功能:一个是匹配验证。另一个是密码编码。
任何应用考虑到安全,绝不能明文的方式保存密码。密码应该通过哈希算法进行加密。 有很多标准的算法比如SHA或者MD5,结合salt(盐)是一个不错的选择。 Spring Security 提供了BCryptPasswordEncoder类,实现Spring的PasswordEncoder接口使用BCrypt强哈希方法来加密密码。BCrypt强哈希方法 每次加密的结果都不一样,所以更加的安全。
bcrypt算法相对来说是运算比较慢的算法,在密码学界有句常话:越慢的算法越安全。黑客破解成本越高.通过salt和const这两个值来减缓加密过程,它的加密时间(百ms级)远远超过md5(大概1ms左右)。对于计算机来说,Bcrypt 的计算速度很慢,但是对于用户来说,这个过程不算慢。bcrypt是单向的,而且经过salt和cost的处理,使其受攻击破解的概率大大降低,同时破解的难度也提升不少,相对于MD5等加密方式更加安全,而且使用也比较简单
bcrypt加密后的字符串形如:
$2a$10$wouq9P/HNgvYj2jKtUN8rOJJNRVCWvn1XoWy55N3sCkEHZPo3lyWq
其中$是分割符,无意义;2a是bcrypt加密版本号;10是const的值;而后的前22位是salt值;再然后的字符串就是密码的密文了;这里的const值即生成salt的迭代次数,默认值是10,推荐值12。
首先看下PasswordEncoderFactories 密码器工厂
之前我们在项目中密码使用的是明文的是noop , 代表不加密使用明文密码, 现在用BCrypt只需要将noop 换成bcrypt 即可 ,在MyUserDetailsService 自定义认证类去修改
同时需要将数据库中的明文密码修改为加密密码
选择一个放入数据库即可
在传统web系统中, 我们将登录成功的用户放入session中, 在需要的时候可以从session中获取用户,那么Spring Security中我们如何获取当前已经登录的用户呢?
SecurityContextHolder:保留系统当前的安全上下文SecurityContext,其中就包括当前使用系统的用户的信息。
SecurityContext:安全上下文,获取当前经过身份验证的主体或身份验证请求令牌
代码实现:
/**
* 获取当前登录用户
*/
@GetMapping("/loginUser")
@ResponseBody
public UserDetails getCurrentUser() {
UserDetails principal = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return principal;
}
除了上述方法, Spring Security 还提供了2种方式可以获取
/**
* 获取当前登录用户
*/
@GetMapping("/loginUser2")
@ResponseBody
public UserDetails getCurrentUser2(Authentication authentication) {
UserDetails principal = (UserDetails) authentication.getPrincipal();
return principal;
}
/**
* 获取当前登录用户
*/
@GetMapping("/loginUser3")
@ResponseBody
public UserDetails getCurrentUser3(@AuthenticationPrincipal UserDetails userDetails) {
return userDetails;
}
在大多数网站中,都会实现RememberMe这个功能,方便用户在下一次登录时直接登录,避免再次输入用户名以及密码去登录,Spring Security针对这个功能已经帮助我们实现, 下面我们来看下他的原理图
Token=MD5(username+分隔符+expiryTime+分隔符+password)
注意: 这种方式不推荐使用, 有严重的安全问题. 就是密码信息在前端浏览器cookie中存放. 如果cookie被盗取很容易破解
代码实现:
1. 前端页面需要增加remember-me的复选框
记住我
2. 后台代码开启remember-me功能
.and().rememberMe()//开启记住我功能
.tokenValiditySeconds(1209600)// token失效时间默认2周
.rememberMeParameter("remember-me")// 自定义表单name值
3. 登录成功后前台cookie
存入数据库Token包含:
token: 随机生成策略,每次访问都会重新生成
series: 登录序列号,随机生成策略。用户输入用户名和密码登录时,该值重新生成。使用remember-me功能,该值保持不变
expiryTime: token过期时间。
CookieValue=encode(series+token)
代码实现:
1. 后台代码
@Override
protected void configure(HttpSecurity http) throws Exception {
/* http.httpBasic() // 开启httpBasic认证
.and().authorizeRequests().anyRequest().authenticated(); // 所有请求都需要认证后才能访问 */
http.formLogin()// 开启表单认证
.loginPage("/toLoginPage")// 自定义登录页面
.loginProcessingUrl("/login")// 登录处理Url
.usernameParameter("username")
.passwordParameter("password") // 自定义input的name值
.successForwardUrl("/")// 登录成功后跳转路径
.and().authorizeRequests()
.antMatchers("/toLoginPage").permitAll()// 放行登录页面与静态资源
.anyRequest().authenticated()// 所有请求都需要登录认证才能访问;
.and().rememberMe()// 开启记住我功能
.tokenValiditySeconds(1209600)// token失效时间默认2周
.rememberMeParameter("remember-me")// 自定义表单name值
.tokenRepository(getPersistentTokenRepository());// 设置tokenRepository
// 关闭csrf防护
http.csrf().disable();
// 允许iframe加载页面
http.headers().frameOptions().sameOrigin();
}
@Autowired
DataSource dataSource;
/**
* 持久化token,负责token与数据库之间的相关操作
*
* @return
*/
@Bean
public PersistentTokenRepository getPersistentTokenRepository() {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource); // 设置数据源
// 启动时创建一张表, 第一次启动的时候创建, 第二次启动的时候需要注释掉或设置为false, 否则会报错
tokenRepository.setCreateTableOnStartup(true);
return tokenRepository;
}
项目启动成功后,观察数据库,会帮助我们创建persistent_logins表
2. 再次完成登录功能
在观察cookie值
3. Cookie窃取伪造演示
4、安全验证
如果有些方法非常重要,而我们为了避免cookies窃取登录,这个时候我们就可以设置一个方法,让该方法只能通过输入密码登录后访问,而cookies方式不能进行访问。这样就可以很大程度的避免了,重要的方法被窃取cookies访问,保证了安全。
/**
* 根据用户ID查询用户
*
* @return
*/
@GetMapping("/{id}")
@ResponseBody
public User getById(@PathVariable Integer id) {
// 获取认证的信息
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// 如果返回true 代表这个登录认证信息来源于自动登录
if (RememberMeAuthenticationToken.class.isAssignableFrom(authentication.getClass())) {
throw new RememberMeAuthenticationException("认证来源于RememberMe");
}
User user = userService.getById(id);
return user;
}
在重要操作步骤可以加以验证, true代表自动登录,则引导用户重新表单登录, false正常进行
在某些场景下,用户登录成功或失败的情况下用户需要执行一些后续操作,比如登录日志的搜集,或者在现在目前前后端分离的情况下用户登录成功和失败后需要给前台页面返回对应的错误信息, 有前台主导登录成功或者失败的页面跳转. 这个时候需要要到用到AuthenticationSuccessHandler与AnthenticationFailureHandler
自定义成功处理:
实现AuthenticationSuccessHandler接口,并重写onAnthenticationSuccesss()方法
自定义失败处理:
实现AuthenticationFailureHandler接口,并重写onAuthenticationFailure()方法
- SecurityConfiguration类
```java
.successHandler(myAuthenticationService) // 登录成功后的处理
.failureHandler(myAuthenticationService) // 登录失败后的处理
package com.lagou.service.impl;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Service;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* 自定义登录成功或失败处理器,退出登录处理器
*/
@Service
public class MyAuthenticationService implements AuthenticationSuccessHandler,
AuthenticationFailureHandler{
// 这个工具类,可以方便的帮助我们跳转页面
RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
/**
* 登录成功后的处理逻辑
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
System.out.println("登录成功后,继续处理.............");
// 重定向到index页面
redirectStrategy.sendRedirect(request, response, "/");
}
/**
* 登录失败后的处理逻辑
*/
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
System.out.println("登录失败后继续处理...............");
// 重定向到login页面
redirectStrategy.sendRedirect(request, response, "/toLoginPage");
}
}
这种方式就是使用ajax进行表单的提交,并且页面跳转的工作交给了前端,而我们后端则是进行逻辑处理工作,最后将响应的数据发送给前端,前端接收到传来的数据后,然后对数据进行分析展示,最后跳转页面。
前端页面改造
MyAuthenticationService类改造
package com.lagou.service.impl;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Service;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* 自定义登录成功或失败处理器,退出登录处理器
*/
@Service
public class MyAuthenticationService implements AuthenticationSuccessHandler,
AuthenticationFailureHandler {
// 这个工具类,可以方便的帮助我们跳转页面
RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@Autowired
ObjectMapper objectMapper;
/**
* 登录成功后的处理逻辑
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
System.out.println("登录成功后,继续处理.............");
// 重定向到index页面
// redirectStrategy.sendRedirect(request, response, "/");
Map
org.springframework.security.web.authentication.logout.LogoutFilter
匹配URL为/logout的请求,实现用户退出,清除认证信息。
只需要发送请求,请求路径为/logout即可, 当然这个路径也可以自行在配置类中自行指定, 同时退出操作也有对应的自定义处理方法,实现LogoutSuccessHandler 接口重写里面的方法,退出登录成功后执行,退出的同时如果有remember-me的数据,同时一并删除
前端页面
后台代码MyAuthenticationService
package com.lagou.service.impl;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Service;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* 自定义登录成功或失败处理器,退出登录处理器
*/
@Service
public class MyAuthenticationService implements AuthenticationSuccessHandler,
AuthenticationFailureHandler, LogoutSuccessHandler {
// 这个工具类,可以方便的帮助我们跳转页面
RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@Autowired
ObjectMapper objectMapper;
/**
* 登录成功后的处理逻辑
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
System.out.println("登录成功后,继续处理.............");
// 重定向到index页面
// 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));
}
/**
* 登录失败后的处理逻辑
*/
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
System.out.println("登录失败后继续处理...............");
// 重定向到login页面
// 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");
}
}
在SecurityConfig类中添加属性
.and().logout().logoutUrl("/logout")//设置退出url
.logoutSuccessHandler(myAuthenticationService)//自定义退出处理器
package com.lagou.config;
import com.lagou.service.impl.MyAuthenticationService;
import com.lagou.service.impl.MyUserDetailsService;
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.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import javax.sql.DataSource;
/**
* spring security配置类
*/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
MyUserDetailsService myUserDetailsService;
@Autowired
MyAuthenticationService myAuthenticationService;
/**
* 身份安全管理器
*
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailsService);
}
@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值
.successForwardUrl("/") // 登录成功之后跳转的路径
.successHandler(myAuthenticationService) // 登录成功后的处理
.failureHandler(myAuthenticationService) // 登录失败后的处理
.and().logout().logoutUrl("/logout") // 指定退出路径,默认为/logout
.logoutSuccessHandler(myAuthenticationService) // 退出之后的处理逻辑
.and().rememberMe() // 开启记住我功能
.tokenValiditySeconds(1209600) // token失效时间,默认是两周
.rememberMeParameter("remember-me") // 表示表单input里面的name值,不写默认就是remember-me
.tokenRepository(getPersistentTokenRepository())
.and().authorizeRequests().antMatchers("/toLoginPage").permitAll() // 放行登录页面
.anyRequest().authenticated();
// 关闭csrf防护
http.csrf().disable();
// 加载同源域名下的iframe页面
http.headers().frameOptions().sameOrigin();
}
/**
* 负责token与数据库之间的操作
*
* @return
*/
@Autowired
DataSource dataSource;
@Bean
public PersistentTokenRepository getPersistentTokenRepository() {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource); // 设置数据源
tokenRepository.setCreateTableOnStartup(false); // 启动时自动帮我们创建一张表,第一次启动设置true,第二次启动设置为false或者注释掉
return tokenRepository;
}
}