Spring-Security
Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。它是用于保护基于Spring的应用程序的实际标准。
Spring Security是一个框架,致力于为Java应用程序提供身份验证和授权。与所有Spring项目一样,Spring Security的真正强大之处在于可以轻松扩展以满足自定义要求
SpringSecurity基本原理就是使用大量的过滤器形成一个过滤器链,完成安全认证。
主要的过滤器有登陆验证过滤器,权限验证过滤器等,我们也可以增加我们自己的过滤器完成定制化开发。
请求响应时,先尝试从session中获取用户的登陆信息如果获取成功,将用户信息保存到ThreadLocal中,如果session中没有获取到用户信息依次走各个过滤器,最终将登陆成功后的用户信息存储到一个ThreadLocal中,如果执行过滤器代码时发生异常,会统一被ExceptionTranslationFilter捕获并进行处理。如果没有发生异常,最终通过橙色的FilterSecurityInterceptor拦截器后访问后台的rest服务。
SpringBoot集成SpringSecurity
- 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
此时已经成功引入,启动项目直接访问项目地址会出现认证登录的页面
用户名为user,密码会在控制台打印
- 使用自己的用户名和密码
实现UserDetailsService
接口
该接口的loadUserByUsername方法需要返回一个UserDetail实例,
SpringSecurity提供的User对象已经对该接口进行了实现。
User类有两个构造方法,
- 第一个是三个参数的构造方法:
public User(String username, String password,
Collection<? extends GrantedAuthority> authorities) {
this(username, password, true, true, true, true, authorities);
}
第一个构造参数为传入的用户名,第二个参数为用户的密码,(这里为了方便演示,直接写死,真正开发中需要从数据库中查询)第三个参数是用户所具备的权限。
- 第二个是七个参数的构造方法
先看一下UserDetail接口
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
该接口除了提供获取用户名密码和权限的接口外,还提供了验证用户是否过期,是否被锁定,是否冻结以及是否可用的方法。User类的第二个构造方法新增的四个参数和以上四个方法一一对应。当我们需要使用这些功能时直接使用即可。
@Slf4j
@Component
public class MyUserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
log.info("用户名:{}", s);
return new User(s, "123456",AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
重新启动项目,登录会发生如下异常:
原因是由于Spring boot 2.0.3引用的security 依赖是 spring security 5.X版本,此版本需要提供一个PasswordEncorder的实例。
- 提供PasswordEncoder实例
PasswordEncoder接口提供两个方法,分别是对密码进行编码和对密码进行匹配校验。
这里暂时不使用任何编码方式,不做任何处理。再次启动项目后,可以登录成功。
@Component
public class MyPasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence charSequence) {
return charSequence.toString();
}
@Override
public boolean matches(CharSequence charSequence, String s) {
return s.equals(charSequence.toString());
}
}
自定义用户认证流程
自定义用户认证页面
- 在resources目录下的static文件夹中新增login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>自定义用户认证逻辑</title>
</head>
<body>
<h2>标准认证页面</h2>
<form action="/user/login" method="post">
<table>
<tr>
<td>用户名:</td>
<td><input type="text" name="username"></td>
</tr>
<tr>
<td>密码:</td>
<td><input type="password" name="password"></td>
</tr>
<tr>
<td colspan="2"><button type="submit">登录</button></td>
</tr>
</table>
</form>
</body>
</html>
- 新增WebSecurityConfigurerAdapter配置类,覆盖
configure(HttpSecurity http) throws Exception
方法
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 更改为任何请求都需要表单登录并且任何请求都需要授权
*
* @param http http
* @throws Exception exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() // 表单登录
.loginPage("/login.html") // 增加自定义认证页面路径
.and()
.authorizeRequests() // 任何请求都需要认证
.anyRequest()
.authenticated();
}
}
启动项目,访问项目地址会出现如下页面
原因是上面的配置的意思为所有的页面都需要认证,认证的页面为login.html而login.html也需要认证,导致页面递归跳转,最终发生以上问题。解决方式就是将认证页面放行,认证页面不需要认证。
将上面的配置修改为:
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 更改为任何请求都需要表单登录并且任何请求都需要授权
*
* @param http http
* @throws Exception exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() // 表单登录
.loginPage("/login.html") // 增加自定义认证页面路径
.loginProcessingUrl("/user/login")
.and()
.authorizeRequests() // 任何请求都需要认证
.antMatchers("/login.html").permitAll()
.anyRequest()
.authenticated()
.and()
.csrf()
.disable();
}
}
自定义认证规则
自定义登录认证页面
如果请求的是html页面,那么返回html登录页,如果是访问接口数据,返回用户需要登录json信息
此处使用到的配置类在最后会给出
- 创建BrowserSecurityController类
public class BrowserSecurityController {
private HttpSessionRequestCache httpSessionRequestCache = new HttpSessionRequestCache();
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@Resource
private SecurityProperties securityProperties;
/**
* 当需要身份认证时跳转到这里
*
* @param request
* @param response
* @return
* @throws IOException
*/
@GetMapping("/authentication/require")
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public ResultVO<String> requireAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException {
SavedRequest savedRequest = httpSessionRequestCache.getRequest(request, response);
if (Objects.nonNull(savedRequest)) {
String redirectUrl = savedRequest.getRedirectUrl();
log.info("引发跳转请求URL「{}」", redirectUrl);
if (StringUtils.endsWithIgnoreCase(redirectUrl, ".html")) {
redirectStrategy.sendRedirect(request, response, securityProperties.getBrowser().getLoginPage());
}
}
return ResultVO.buildFailure("需要用户认证");
}
}
- 更换认证配置
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() // 表单登录
.loginPage("/authentication/require") // 增加自定义认证页面路径
.loginProcessingUrl("/user/login")
.and()
.authorizeRequests() // 任何请求都需要认证
.antMatchers("/authentication/require").permitAll()
.anyRequest()
.authenticated()
.and()
.csrf()
.disable();
}
自定义登录成功/失败处理
[需求]通过配置文件进行配置,如果配置的json的形式,返回json,如果配置的是redirect形式,跳转到响应的页面
如果不需要刚才的实现,只需要实现AuthenticationSuccessHandler接口,即可,但是SpringSecurity默认的处理是跳转到指定的页面,所以我们可以继承原来的逻辑,并通过判断决策使用那种方式即可。
- 成功处理
@Slf4j
@Component
public class CustomerAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler /**AuthenticationSuccessHandler*/
{
@Resource
private ObjectMapper objectMapper;
@Resource
private SecurityProperties securityProperties;
/**
* 登录成功处理,这里登录成功后,将{@link Authentication}返回给前端
*
* @param request
* @param response
* @param authentication
* @throws IOException
* @throws ServletException
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
log.info("登录成功");
LoginType loginType = securityProperties.getBrowser().getLoginType();
if (LoginType.JSON.equals(loginType)) {
response.setContentType(MediaType.APPLICATION_JSON.getType());
response.getWriter().write(objectMapper.writeValueAsString(authentication));
} else {
super.onAuthenticationSuccess(request, response, authentication);
}
}
}
- 失败处理
@Slf4j
@Component
public class CustomerAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler /**AuthenticationFailureHandler*/ {
@Resource
private ObjectMapper objectMapper;
@Resource
private SecurityProperties securityProperties;
/**
* 失败处理,这里如果返回的格式为json,返回异常的json串
* @param request
* @param response
* @param e
* @throws IOException
* @throws ServletException
*/
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
log.info("登录失败");
LoginType loginType = securityProperties.getBrowser().getLoginType();
if(LoginType.JSON.equals(loginType)){
response.setStatus(HttpStatus.BAD_REQUEST.value());
response.getWriter().write(objectMapper.writeValueAsString(e));
}else{
super.onAuthenticationFailure(request, response, e);
}
}
}
- 通过配置类替换原有的类
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private SecurityProperties securityProperties;
@Resource
private CustomerAuthenticationSuccessHandler customerAuthenticationSuccessHandler;
@Resource
private CustomerAuthenticationFailureHandler customerAuthenticationFailureHandler;
/**
* 更改为任何请求都需要表单登录并且任何请求都需要授权
*
* @param http http
* @throws Exception exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() // 表单登录
.loginPage("/authentication/require") // 增加自定义认证页面路径
.loginProcessingUrl("/user/login")
.successHandler(customerAuthenticationSuccessHandler)
.failureHandler(customerAuthenticationFailureHandler)
.and()
.authorizeRequests() // 任何请求都需要认证
.antMatchers("/login.html",
"/authentication/require",
securityProperties.getBrowser().getLoginPage()
).permitAll()
.anyRequest()
.authenticated()
.and()
.csrf()
.disable();
}
}
获取登陆成功后的用户信息
@GetMapping("/getUserInfo")
public ResultVO<Authentication> getUserInfo(){
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return ResultVO.buildSuccess(authentication);
}
配置类
- SecurityCoreConfig
@Configuration
@EnableConfigurationProperties(SecurityProperties.class)
public class SecurityCoreConfig {
}
- SecurityProperties
@Data
@ConfigurationProperties(prefix = "customer.security")
public class SecurityProperties {
/**
* 浏览器相关配置
*/
private BrowserProperties browser = new BrowserProperties();
}
- BrowserProperties
@Data
public class BrowserProperties {
/**
* 用户自定义登录页面
*/
private String loginPage = "/login.html";
/**
* 登陆成功响应方式{@link LoginType}
*/
private LoginType loginType = LoginType.JSON;
}
参考文章: