前言
在没有Spring Security等权限控制的情况下,任何人都可以访问我们的Rest服务,现在我们需要对我们的Rest服务进行保护。让指定的人能够访问,并且指定的人也只能访问若干Rest服务
核心功能
- 认证
识别当前用户是否合法
即:你是谁
- 授权
当前用户能访问的数据、页面权限
即:你能干什么
- 攻击防护
防止伪造身份
SpringSecurity基本原理与使用
Spring Security的默认行为
#配置Spring Session存储,
spring.session.store-type=none
#是否使用SpringSecurity
security.basic.enabled = true
上述配置打开了SpringSecurity,
此时我们再去访问我们的Restful接口也好,还是通过浏览器进行页面访问也好,
都会被拦截掉,弹出一个登录页面,
默认用户名:user
,密码可以在应用启动日志中看到
在引入了基于SpringBoot的SpringSecurity,其默认行为会拦截保护所有对服务器的访问。
每个请求都需要用户名/密码
认证
这种由SpringSecurity提供的默认校验方式叫做httpBasic
,
实际环境中基本没用,所以我们往往需要自定义
表单验证
默认情况下,httpbasic的认证方式,是一个弹出窗口,下面我们
使用Spring Security提供的表单进行认证
package com.imooc.security.browser;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
* WebSecurityConfigurerAdapter是专门用于配置web安全应用的适配器
*
* @Author 张柳宁
* @Description
* @Date Create in 2018/3/21
* @Modified By:
*/
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// 指定认证方式为表单认证
// 任何请求都需要身份认证
http.formLogin()//认证方式
.and()
.authorizeRequests()//权限配置
.anyRequest()//任何请求
.authenticated();//都需要身份认证
}
}
此时,我们再访问服务器,就会跳转到一个表单登陆页,而不是弹窗
image.png
这个页面仍旧是由SpringSecurity为我们提供的,我们肯定需要自定义登陆页面的,自定义页面我们后续再讲
注意,这里的
BrowserSecurityConfig
配置文件一定要能够被扫描到,或者被引入,否则无法生效
@SpringBootApplication
@Import(BrowserSecurityConfig.class)//引入配置文件
@RestController
@EnableSwagger2
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@GetMapping("/hello")
public String hello(){
return "Hello Spring Security";
}
}
弹窗验证
使用弹窗登陆验证窗口
即默认方式
@Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic()
.and()
.authorizeRequests()
.anyRequest()
.authenticated();
}
配置认证方式为httpBasic
即可
基本原理
经过上面的两个案例,我们对Spring Security有了一个简单直观的认识,下面对Spring Security的原理做一个讲解。
-
过滤器
Spring Security其底层是通过Filter-过滤器
实现的
所有的请求和响应都会经过Spring Security的一系列过滤器,达到权限控制的效果。 -
过滤器在Spring Boot中的配置
Spring Security的过滤器在Spring Boot启动的时候就会被配置进去
绿色
部分为一系列的过滤器,如果请求中有相应的参数,则进行校验,没有就放行。
每个绿色的过滤器表示一种认证方式。
蓝色
用于捕获橙色
抛出来的异常,当认证不通过
的时候,橙色上是会抛出异常的。
橙色
为最后一个过滤器,其决定了本次请求是否有权限访问我们的系统服务。
其判断依据为我们配置的认证情况与实际当前过滤器链中的认证情况。
比如:我们配置了要表单验证,可是实际上并没有通过表单验证,则不会放行。
- 过滤器类型
只有图中绿色的过滤器是可以由我们控制是否生效,并且支持自定义的。另外两种类型的过滤器不是由我们控制的
自定义认证逻辑
在上节中,正确的用户名和密码是由Spring Security生成的。在实际开发中这个肯定是要自定义的
接口:UserDetailsService、UserDetails
UserDetailsService
:接口用于获取用户身份信息
UserDetails
:接口用于保存获取到的用户信息
org.springframework.security.core.userdetails.User
:是Spring Security为我们提供的对UserDetails
的一个实现。
UserDetails
接口还提供了4个方法,可以由我们自定义实现
方法 | 说明 |
---|---|
isAccountNonExpired | 用户是否未过期 |
isAccountNonLocked | 用户是否未锁定,冻结 |
isCredentialsNonExpired | 密码是否未过期 |
isEnabled | 用户是否未可用,被删除 |
关于isAccountNonLocked
与isEnabled
,一般再业务上,一个表示冻结,一个表示删除。
其实都只是两个状态,删除也只是打一个标记位,一般不会真的删除。
被冻结的一般可以被恢复,删除的就不恢复了。
在我们自己实现UserDetails
的时候,如果上述四个方法返回false
,就表示不通过
自定义校验逻辑
package com.imooc.security.browser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.Component;
@Component
public class MyUserDetailsService implements UserDetailsService {
private Logger logger = LoggerFactory.getLogger(MyUserDetailsService.class);
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
logger.info("查找登陆用户名:{}", username);
//模拟根据用户名查找用户信息
return new User(username, "123456", AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
username
为前端传递过来的用户名,我们从数据库中查询出这个用户的权限信息,
AuthorityUtils.commaSeparatedStringToAuthorityList("admin")
将角色字符串转化成所需的类型
使用User
对象封装后进行返回,Spring Security
会将返回的真实认证信息与用户输入的认证信息进行比较,如果不相符,
如果我们使用User
的另一个构造函数
return new User(username,"123456",false,false,false,false, AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
那么登陆返回就是
接口:PasswordEncoder
数据库中一般不会存储明文密码
package org.springframework.security.crypto.password;
public interface PasswordEncoder {
String encode(CharSequence rawPassword);
boolean matches(CharSequence rawPassword, String encodedPassword);
}
方法 | 说明 |
---|---|
encode | 加密 |
matches | 判断密码是否正确 |
一般在新增用户的时候,调用encode
,将用户的密码加密后入库
matches
一般由Spring Security
进行调用
把我们返回的 UserDetails
对象中的密码与本次用户输入的进行比较
在上述中,我们使用123456
作为密码返回,这个不是密文,用户登陆的时候输入的也是123456
,竟然可以通过。
这是因为我们当前还没配置过PasswordEncoder
,我们配置一个Spring Security
为我们提供的PasswordEncoder
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
此时,我们在UserDetails
对象中的密码就必须是用BCryptPasswordEncoder
加密过的密文才行了
package com.imooc.security.browser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.Component;
/**
* 自定义认证逻辑
*/
@Component
public class MyUserDetailsService implements UserDetailsService {
private Logger logger = LoggerFactory.getLogger(MyUserDetailsService.class);
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
logger.info("查找登陆用户名:{}", username);
//模拟根据用户名查找用户信息
//这个密码是从数据库中读出来的
String password = passwordEncoder.encode("123456");
System.out.println("密码:" + password);
return new User(username, password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
Spring Security将用户输入的密码,与返回的加密密码,通过调用matches方法进行校验。
自定义认证页面
浏览器访问网站的时候,不同网站的登陆页面肯定是不同的,我们下面不再使用Spring Security提供的表单,换成自定义的html页面作为认证页面
认证页面
- imooc-signIn.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<h2>标准登录页面</h2>
<h3>表单登录</h3>
<form action="/authentication/form" 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>
- BrowserSecurityConfig
package com.imooc.security.browser;
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;
/**
* WebSecurityConfigurerAdapter是专门用于配置web安全应用的适配器
*
* @Author 张柳宁
* @Description
* @Date Create in 2018/3/21
* @Modified By:
*/
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/imooc-signIn.html")
.loginProcessingUrl("/authentication/form")
.and()
.authorizeRequests()
.antMatchers("/imooc-signIn.html").permitAll()
.anyRequest()
.authenticated()
.and()
.csrf()
.disable();
}
}
.loginPage("/imooc-signIn.html"):配置认证页面所在url
.loginProcessingUrl("/authentication/form"):配置处理认证请求的地址
.antMatchers("/imooc-signIn.html").permitAll():跳转认证页面的请求要忽略认证
.and().csrf().disable():忽略CSRF认证,是Spring Boot Security默认配置的一个安全项(跨站请求防护),暂时关闭
认证类型判断
一般服务端会根据不同的客户端请求,返回不同的认证信息。
当检测到需要认证的时候,我们判断当前客户端是浏览器,就给他个html登陆页面;
当检测到当前访问者是app的时候,就返回json数据告诉他先认证
image.pngBrowserSecurityController
package com.imooc.security.browser;
import com.imooc.security.browser.support.SimpleResponse;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.savedrequest.SavedRequest;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @Author 张柳宁
* @Description
* @Date Create in 2018/4/9
* @Modified By:
*/
@RestController
public class BrowserSecurityController {
private Logger logger = LoggerFactory.getLogger(BrowserSecurityController.class);
//请求的缓存对象
private RequestCache requestCache = new HttpSessionRequestCache();
//跳转工具类
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
/**
* 当需要身份认证时,跳转到此请求
*
* @param request
* @param response
* @return
*/
@RequestMapping("/authentication/require")
@ResponseStatus(code = HttpStatus.UNAUTHORIZED)//返回401状态码,表示未授权
public SimpleResponse requireAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException {
SavedRequest savedRequest = requestCache.getRequest(request, response);
if (savedRequest != null) {
String target = savedRequest.getRedirectUrl();//引发跳转的url
logger.info("引发跳转的URL:{}", target);
if (StringUtils.endsWithIgnoreCase(target, ".html")) {//如果引发跳转的url后缀为html,则跳转到html登陆页面
//跳转到自定义配置的登陆页面
redirectStrategy.sendRedirect(request, response, "/imooc-signIn.html");
}
}
return new SimpleResponse("访问的服务需要身份认证");
}
}
BrowserSecurityConfig
package com.imooc.security.browser;
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;
/**
* WebSecurityConfigurerAdapter是专门用于配置web安全应用的适配器
*
* @Author 张柳宁
* @Description
* @Date Create in 2018/3/21
* @Modified By:
*/
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/authentication/require")
.loginProcessingUrl("/authentication/form")
.and()
.authorizeRequests()
.antMatchers("/authentication/require", "/imooc-signIn.html").permitAll()
.anyRequest()
.authenticated()
.and()
.csrf()
.disable();
}
}
.loginPage("/authentication/require"):当检测到请求需要认证的时候,跳转到我们的自定义控制器上
优化:登陆页可配置
application.yml
server:
port: 8060
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/imooc-demo?useUnicode=yes&characterEncoding=UTF-8&useSSL=false
username: root
password: 123456
session:
# session集群管理,none表示关闭
store-type: none
security:
basic:
# 开启Spring Security
enabled: true
imooc:
security:
loginPage: /demo-signIn.html
BrowserProperties
package com.imooc.security.core.properties;
/**
* @Author 张柳宁
* @Description
* @Date Create in 2018/3/24
* @Modified By:
*/
public class BrowserProperties {
private String loginPage = "/imooc-signIn.html";
public String getLoginPage() {
return loginPage;
}
public void setLoginPage(String loginPage) {
this.loginPage = loginPage;
}
}
SecurityProperties
package com.imooc.security.core.properties;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @Author 张柳宁
* @Description
* @Date Create in 2018/3/24
* @Modified By:
*/
@ConfigurationProperties(prefix = "imooc.security")
public class SecurityProperties {
private BrowserProperties browser = new BrowserProperties();
public BrowserProperties getBrowser() {
return browser;
}
public void setBrowser(BrowserProperties browser) {
this.browser = browser;
}
}
SecurityCoreConfig
package com.imooc.security.core.properties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* @Author 张柳宁
* @Description
* @Date Create in 2018/3/24
* @Modified By:
*/
@Configuration
@EnableConfigurationProperties(SecurityProperties.class)//使配置文件生效
public class SecurityCoreConfig {
}
认证成功后处理
默认情况下,认证成功后跳转到引发认证的请求url
如:我因为访问系统的 /user,被SpringSecurity拦截,跳转到了登陆页面,那么登陆成功后,就会再次访问 /user
实际情况下,更多的是登陆成功后需要进行一系列ajax请求,此时就需要自定义登陆成功后的处理
AuthenticationSuccessHandler
- 第一步:实现接口
package com.imooc.security.browser.authentication;
import com.imooc.security.browser.utils.JsonUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 自定义登陆成功后的处理
* @Author 张柳宁
* @Description
* @Date Create in 2018/3/27
* @Modified By:
*/
@Component
public class ImoocAuthencationSuccessHandler implements AuthenticationSuccessHandler {
private Logger logger = LoggerFactory.getLogger(ImoocAuthencationSuccessHandler.class);
/**
* 登陆成功后调用
* @param request
* @param response
* @param authentication 封装认证信息
* @throws IOException
* @throws ServletException
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
logger.info("登陆成功");
String jsonStr = JsonUtils.obj2Json(authentication);
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(jsonStr);
}
}
这里我们就是简单的将对象转化为json后打印在控制台
- 第二步:配置
package com.imooc.security.browser;
import com.imooc.security.browser.authentication.ImoocAuthencationSuccessHandler;
import com.imooc.security.core.properties.SecurityProperties;
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;
/**
* WebSecurityConfigurerAdapter是专门用于配置web安全应用的适配器
*
* @Author 张柳宁
* @Description
* @Date Create in 2018/3/21
* @Modified By:
*/
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SecurityProperties securityProperties;
@Autowired
private ImoocAuthencationSuccessHandler imoocAuthencationSuccessHandler;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/authentication/require")
.loginProcessingUrl("/authentication/form")
.successHandler(imoocAuthencationSuccessHandler)
.and()
.authorizeRequests()
.antMatchers("/authentication/require", securityProperties.getBrowser().getLoginPage()).permitAll()
.anyRequest()
.authenticated()
.and()
.csrf()
.disable();
}
}
主要是.successHandler(imoocAuthencationSuccessHandler)
-
第三步:验证
登陆成功后不再是默认处理,而是按照我们的自定义处理
image.png
认证失败后处理
AuthenticationFailureHandler
- 第一步:实现接口
package com.imooc.security.browser.authentication;
import com.imooc.security.browser.utils.JsonUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @Author 张柳宁
* @Description
* @Date Create in 2018/3/27
* @Modified By:
*/
@Component
public class ImoocAuthencationFailureHandler implements AuthenticationFailureHandler {
private Logger logger = LoggerFactory.getLogger(ImoocAuthencationFailureHandler.class);
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
logger.info("登陆失败");
String jsonStr = JsonUtils.obj2Json(exception);
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(jsonStr);
}
}
- 第二步:配置
package com.imooc.security.browser;
import com.imooc.security.browser.authentication.ImoocAuthencationFailureHandler;
import com.imooc.security.browser.authentication.ImoocAuthencationSuccessHandler;
import com.imooc.security.core.properties.SecurityProperties;
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;
/**
* WebSecurityConfigurerAdapter是专门用于配置web安全应用的适配器
*
* @Author 张柳宁
* @Description
* @Date Create in 2018/3/21
* @Modified By:
*/
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SecurityProperties securityProperties;
@Autowired
private ImoocAuthencationSuccessHandler imoocAuthencationSuccessHandler;
@Autowired
private ImoocAuthencationFailureHandler imoocAuthencationFailureHandler;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/authentication/require")
.loginProcessingUrl("/authentication/form")
.successHandler(imoocAuthencationSuccessHandler)
.failureHandler(imoocAuthencationFailureHandler)
.and()
.authorizeRequests()
.antMatchers("/authentication/require", securityProperties.getBrowser().getLoginPage()).permitAll()
.anyRequest()
.authenticated()
.and()
.csrf()
.disable();
}
}
-
第三步:验证
当我们输入错误的用户名密码的时候,就会按照我们指定的方式执行
扩展
目前我们对成功或者失败的处理,都是返回json,
现在我们需要根据配置,决定是页面跳转还是返回json
自定义登陆类型
package com.imooc.security.core.properties;
public enum LoginType {
REDIRECT,
JSON
}
ImoocAuthencationSuccessHandler
package com.imooc.security.browser.authentication;
import com.imooc.security.browser.utils.JsonUtils;
import com.imooc.security.core.properties.LoginType;
import com.imooc.security.core.properties.SecurityProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 自定义登陆成功后的处理
* @Author 张柳宁
* @Description
* @Date Create in 2018/3/27
* @Modified By:
*/
@Component
public class ImoocAuthencationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
private Logger logger = LoggerFactory.getLogger(ImoocAuthencationSuccessHandler.class);
@Autowired
private SecurityProperties securityProperties;
/**
* 登陆成功后调用
* @param request
* @param response
* @param authentication 封装认证信息
* @throws IOException
* @throws ServletException
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
logger.info("登陆成功");
if (LoginType.JSON.equals(securityProperties.getBrowser().getLoginType())){
String jsonStr = JsonUtils.obj2Json(authentication);
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(jsonStr);
}else {
super.onAuthenticationSuccess(request,response,authentication);
}
}
}
ImoocAuthencationFailureHandler
package com.imooc.security.browser.authentication;
import com.imooc.security.browser.utils.JsonUtils;
import com.imooc.security.core.properties.LoginType;
import com.imooc.security.core.properties.SecurityProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @Author 张柳宁
* @Description
* @Date Create in 2018/3/27
* @Modified By:
*/
@Component
public class ImoocAuthencationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
private Logger logger = LoggerFactory.getLogger(ImoocAuthencationFailureHandler.class);
@Autowired
private SecurityProperties securityProperties;
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
logger.info("登陆失败");
if (LoginType.JSON.equals(securityProperties.getBrowser().getLoginType())) {
String jsonStr = JsonUtils.obj2Json(exception);
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(jsonStr);
} else {
super.onAuthenticationFailure(request, response, exception);
}
}
}
自定义配置
imooc.security.browser.loginType = REDIRECT
图像验证码
1、根据随机数生成图片
2、将随机数保存到session中
3、将生成的图片写到接口响应中
图片对象
package com.imooc.security.core.validate.code;
import java.awt.image.BufferedImage;
import java.time.LocalDateTime;
/**
* @Author 张柳宁
* @Description
* @Date Create in 2018/3/27
* @Modified By:
*/
public class ImageCode {
//图片
private BufferedImage image;
//验证码
private String code;
//过期时间点
private LocalDateTime expireTime;
public ImageCode() {
}
/**
* 一般使用这个构造函数
*
* @param image
* @param code
* @param expireIn 过期秒数
*/
public ImageCode(BufferedImage image, String code, int expireIn) {
this.image = image;
this.code = code;
this.expireTime = LocalDateTime.now().plusSeconds(expireIn);
}
public ImageCode(BufferedImage image, String code, LocalDateTime expireTime) {
this.image = image;
this.code = code;
this.expireTime = expireTime;
}
public boolean isExpried() {
return LocalDateTime.now().isAfter(expireTime);
}
public BufferedImage getImage() {
return image;
}
public void setImage(BufferedImage image) {
this.image = image;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public LocalDateTime getExpireTime() {
return expireTime;
}
public void setExpireTime(LocalDateTime expireTime) {
this.expireTime = expireTime;
}
}
生成验证码图片的接口
package com.imooc.security.core.validate.code;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.ServletWebRequest;
import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Random;
/**
* @Author 张柳宁
* @Description
* @Date Create in 2018/3/27
* @Modified By:
*/
@RestController
public class ValidateCodeController {
private static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@GetMapping("/code/image")
public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
ImageCode imageCode = createImageCode(request);
sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY, imageCode);
ServletOutputStream sos = response.getOutputStream();
ImageIO.write(imageCode.getImage(), "JPEG", sos);
sos.close();
}
private ImageCode createImageCode(HttpServletRequest request) {
int width = 67;
int height = 23;
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics g = image.getGraphics();
Random random = new Random();
g.setColor(getRandColor(200, 250));
g.fillRect(0, 0, width, height);
g.setFont(new Font("Times New Roman", Font.ITALIC, 20));
g.setColor(getRandColor(160, 200));
for (int i = 0; i < 155; i++) {
int x = random.nextInt(width);
int y = random.nextInt(height);
int xl = random.nextInt(12);
int yl = random.nextInt(12);
g.drawLine(x, y, x + xl, y + yl);
}
String sRand = "";
for (int i = 0; i < 4; i++) {
String rand = String.valueOf(random.nextInt(10));
sRand += rand;
g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
g.drawString(rand, 13 * i + 6, 16);
}
g.dispose();
return new ImageCode(image, sRand, 60);
}
/**
* 生成随机背景条纹
*
* @param fc
* @param bc
* @return
*/
private Color getRandColor(int fc, int bc) {
Random random = new Random();
if (fc > 255) {
fc = 255;
}
if (bc > 255) {
bc = 255;
}
int r = fc + random.nextInt(bc - fc);
int g = fc + random.nextInt(bc - fc);
int b = fc + random.nextInt(bc - fc);
return new Color(r, g, b);
}
}
配置
把生成验证码的url设置为不需要认证
package com.imooc.security.browser;
import com.imooc.security.browser.authentication.ImoocAuthencationFailureHandler;
import com.imooc.security.browser.authentication.ImoocAuthencationSuccessHandler;
import com.imooc.security.core.properties.SecurityProperties;
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;
/**
* WebSecurityConfigurerAdapter是专门用于配置web安全应用的适配器
*
* @Author 张柳宁
* @Description
* @Date Create in 2018/3/21
* @Modified By:
*/
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SecurityProperties securityProperties;
@Autowired
private ImoocAuthencationSuccessHandler imoocAuthencationSuccessHandler;
@Autowired
private ImoocAuthencationFailureHandler imoocAuthencationFailureHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
// 指定认证方式为表单认证
// 任何请求都需要身份认证
http.formLogin()
.loginPage("/authentication/require")//跳转到Controller中,判断返回json还是html
.loginProcessingUrl("/authentication/form")
.successHandler(imoocAuthencationSuccessHandler)
.failureHandler(imoocAuthencationFailureHandler)
.and()
.authorizeRequests()
.antMatchers("/authentication/require"
, securityProperties.getBrowser().getLoginPage()
, "/code/image").permitAll()//忽略请求认证的url
.anyRequest()
.authenticated()
.and()
.csrf()
.disable();
;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
自定义过滤器
package com.imooc.security.core.validate.code;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* OncePerRequestFilter:Spring提供的工具类,保证过滤器仅被调用一次
*
* @Author 张柳宁
* @Description
* @Date Create in 2018/3/27
* @Modified By:
*/
public class ValidateCodeFilter extends OncePerRequestFilter {
private Logger logger = LoggerFactory.getLogger(getClass());
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
private AuthenticationFailureHandler authenticationFailureHandler;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//只有是登陆请求的情况下才进行验证码校验
if (StringUtils.equals("/authentication/form", request.getRequestURI())
&& StringUtils.equalsIgnoreCase("post", request.getMethod())) {
try {
validate(new ServletWebRequest(request));
} catch (ValidateCodeException e) {
logger.error(e.getMessage(), e);
authenticationFailureHandler.onAuthenticationFailure(request, response, e);
// 抛出异常后不继续Filter,直接返回掉
return;
}
}
filterChain.doFilter(request, response);
}
private void validate(ServletWebRequest request) {
ImageCode codeInSession = (ImageCode) sessionStrategy.getAttribute(request, ValidateCodeController.SESSION_KEY);
String codeInRequest = "";
try {
codeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(), "imageCode");
} catch (ServletRequestBindingException e) {
throw new ValidateCodeException("获取验证码的值失败");
}
if (StringUtils.isBlank(codeInRequest)) {
throw new ValidateCodeException("验证码的值不能为空");
}
if (codeInSession == null) {
throw new ValidateCodeException("验证码不存在");
}
if (codeInSession.isExpried()) {
sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY);
throw new ValidateCodeException("验证码已过期");
}
if (!StringUtils.equals(codeInSession.getCode(), codeInRequest)) {
throw new ValidateCodeException("验证码不匹配");
}
sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY);
}
public AuthenticationFailureHandler getAuthenticationFailureHandler() {
return authenticationFailureHandler;
}
public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
this.authenticationFailureHandler = authenticationFailureHandler;
}
}
配置自定义的过滤器
package com.imooc.security.browser;
import com.imooc.security.browser.authentication.ImoocAuthencationFailureHandler;
import com.imooc.security.browser.authentication.ImoocAuthencationSuccessHandler;
import com.imooc.security.core.properties.SecurityProperties;
import com.imooc.security.core.validate.code.ValidateCodeFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
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.UsernamePasswordAuthenticationFilter;
/**
* WebSecurityConfigurerAdapter是专门用于配置web安全应用的适配器
*
* @Author 张柳宁
* @Description
* @Date Create in 2018/3/21
* @Modified By:
*/
@Configuration
@ComponentScan
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SecurityProperties securityProperties;
@Autowired
private ImoocAuthencationSuccessHandler imoocAuthencationSuccessHandler;
@Autowired
private ImoocAuthencationFailureHandler imoocAuthencationFailureHandler;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
validateCodeFilter.setAuthenticationFailureHandler(imoocAuthencationFailureHandler);
http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
.formLogin()
.loginPage("/authentication/require")
.loginProcessingUrl("/authentication/form")
.successHandler(imoocAuthencationSuccessHandler)
.failureHandler(imoocAuthencationFailureHandler)
.and()
.authorizeRequests()
.antMatchers("/authentication/require"
, securityProperties.getBrowser().getLoginPage()
, "/code/image").permitAll()
.anyRequest()
.authenticated()
.and().csrf().disable();
}
}
页面访问
<h3>表单登录</h3>
<form action="/authentication/form" 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>图形验证码:</td>
<td>
<input type="text" name="imageCode">
<img src="/code/image">
</td>
</tr>
<tr>
<td colspan="2">
<button type="submit">登录</button>
</td>
</tr>
</table>
</form>
image.png
优化
添加以下功能
验证码基本参数配置
基本参数配置有3处,依次为core、demo、请求参数
core为核心,demo为当前应用,参数之间有覆盖关系,优先使用后者。
image.png
- 核心默认级别配置
package com.imooc.security.core.properties;
/**
* @author 张柳宁
*
*/
public class ImageCodeProperties {
private int width = 67;
private int height = 23;
private int length = 6;
private int expireIn = 60;
public int getWidth() {
return width;
}
public void setWidth(int width) {
this.width = width;
}
public int getHeight() {
return height;
}
public void setHeight(int height) {
this.height = height;
}
public int getLength() {
return length;
}
public void setLength(int length) {
this.length = length;
}
public int getExpireIn() {
return expireIn;
}
public void setExpireIn(int expireIn) {
this.expireIn = expireIn;
}
}
package com.imooc.security.core.properties;
/**
* @author 张柳宁
*
*/
public class ValidateCodeProperties {
private ImageCodeProperties image = new ImageCodeProperties();
public ImageCodeProperties getImage() {
return image;
}
public void setImage(ImageCodeProperties image) {
this.image = image;
}
}
package com.imooc.security.core.properties;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @Author 张柳宁
* @Description
* @Date Create in 2018/3/24
* @Modified By:
*/
@ConfigurationProperties(prefix = "imooc.security")
public class SecurityProperties {
private BrowserProperties browser = new BrowserProperties();
private ValidateCodeProperties code = new ValidateCodeProperties();
public BrowserProperties getBrowser() {
return browser;
}
public void setBrowser(BrowserProperties browser) {
this.browser = browser;
}
public ValidateCodeProperties getCode() {
return code;
}
public void setCode(ValidateCodeProperties code) {
this.code = code;
}
}
- 应用级别的配置
imooc:
security:
code:
image:
length: 6
- 请求参数级配置
ServletRequestUtils.getIntParameter(request,"width",securityProperties.getCode().getImage().getWidth());
从request中获取指定参数名称的数据,日过没有,就使用应用的配置
在需要获取验证码的页面中
<img src="/code/image?width=200">
通过url把参数传给后台
验证码拦截接口配置
配置哪些请求需要验证码校验
1、在pojo中添加url变量
2、在过滤器中读取配置
3、在配置文件中进行配置
imooc:
security:
code:
image:
url: /user,user/*
package com.imooc.security.core.validate.code;
import com.imooc.security.core.properties.SecurityProperties;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
/**
* OncePerRequestFilter:Spring提供的工具类,保证过滤器仅被调用一次
*
* @Author 张柳宁
* @Description
* @Date Create in 2018/3/27
* @Modified By:
*/
public class ValidateCodeFilter extends OncePerRequestFilter implements InitializingBean {
private Logger logger = LoggerFactory.getLogger(getClass());
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
private AuthenticationFailureHandler authenticationFailureHandler;
private Set<String> urls = new HashSet<>();
private SecurityProperties securityProperties;
// 用于判断Ant形式的字符串匹配规则
private AntPathMatcher pathMatcher = new AntPathMatcher();
@Override
public void afterPropertiesSet() throws ServletException {
super.afterPropertiesSet();
String[] configUrls = StringUtils.splitByWholeSeparator(securityProperties.getCode().getImage().getUrl(), ",");
// 初始化当前配置的需要验证码的url
for (String config : configUrls) {
urls.add(config);
}
urls.add("/authentication/form");//登陆是必须要验证码的
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//只有是登陆请求的情况下才进行验证码校验
boolean action = false;
for (String url : urls) {
if (pathMatcher.match(url, request.getRequestURI())) {
action = true;
break;
}
}
if (action) {
try {
validate(new ServletWebRequest(request));
} catch (ValidateCodeException e) {
logger.error(e.getMessage(), e);
authenticationFailureHandler.onAuthenticationFailure(request, response, e);
// 抛出异常后不继续Filter,直接返回掉
return;
}
}
filterChain.doFilter(request, response);
}
private void validate(ServletWebRequest request) {
ImageCode codeInSession = (ImageCode) sessionStrategy.getAttribute(request, ValidateCodeController.SESSION_KEY);
String codeInRequest = "";
try {
codeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(), "imageCode");
} catch (ServletRequestBindingException e) {
throw new ValidateCodeException("获取验证码的值失败");
}
if (StringUtils.isBlank(codeInRequest)) {
throw new ValidateCodeException("验证码的值不能为空");
}
if (codeInSession == null) {
throw new ValidateCodeException("验证码不存在");
}
if (codeInSession.isExpried()) {
sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY);
throw new ValidateCodeException("验证码已过期");
}
if (!StringUtils.equals(codeInSession.getCode(), codeInRequest)) {
throw new ValidateCodeException("验证码不匹配");
}
sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY);
}
public AuthenticationFailureHandler getAuthenticationFailureHandler() {
return authenticationFailureHandler;
}
public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
this.authenticationFailureHandler = authenticationFailureHandler;
}
public SecurityProperties getSecurityProperties() {
return securityProperties;
}
public void setSecurityProperties(SecurityProperties securityProperties) {
this.securityProperties = securityProperties;
}
public Set<String> getUrls() {
return urls;
}
public void setUrls(Set<String> urls) {
this.urls = urls;
}
}
package com.imooc.security.browser;
import com.imooc.security.browser.authentication.ImoocAuthencationFailureHandler;
import com.imooc.security.browser.authentication.ImoocAuthencationSuccessHandler;
import com.imooc.security.core.properties.SecurityProperties;
import com.imooc.security.core.validate.code.ValidateCodeFilter;
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.UsernamePasswordAuthenticationFilter;
/**
* WebSecurityConfigurerAdapter是专门用于配置web安全应用的适配器
*
* @Author 张柳宁
* @Description
* @Date Create in 2018/3/21
* @Modified By:
*/
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SecurityProperties securityProperties;
@Autowired
private ImoocAuthencationSuccessHandler imoocAuthencationSuccessHandler;
@Autowired
private ImoocAuthencationFailureHandler imoocAuthencationFailureHandler;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
validateCodeFilter.setAuthenticationFailureHandler(imoocAuthencationFailureHandler);
validateCodeFilter.setSecurityProperties(securityProperties);
validateCodeFilter.afterPropertiesSet();
http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
.formLogin()
.loginPage("/authentication/require")
.loginProcessingUrl("/authentication/form")
.successHandler(imoocAuthencationSuccessHandler)
.failureHandler(imoocAuthencationFailureHandler)
.and()
.authorizeRequests()
.antMatchers("/authentication/require"
, securityProperties.getBrowser().getLoginPage()
, "/code/image").permitAll()
.anyRequest()
.authenticated()
.and()
.csrf()
.disable();
}
}
验证码生成逻辑配置
基本思路
1、既然生成逻辑要可定制,那么就不能写死,要使用接口实现的形式
2、使用条件注解,决定使用默认生成逻辑还是用户自己重新写的生成逻辑
package com.imooc.security.core.validate.code;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.imooc.security.core.properties.SecurityProperties;
import com.imooc.security.core.validate.code.image.ImageCodeGenerator;
/**
* @author zhailiang
*
*/
@Configuration
public class ValidateCodeBeanConfig {
@Autowired
private SecurityProperties securityProperties;
@Bean//不存在 名 为 imageCodeGenerator 的Bean,就进行配置
@ConditionalOnMissingBean(name = "imageCodeGenerator")
public ValidateCodeGenerator imageValidateCodeGenerator() {
ImageCodeGenerator codeGenerator = new ImageCodeGenerator();
codeGenerator.setSecurityProperties(securityProperties);
return codeGenerator;
}
}
记住我
这个也是浏览器登陆中一个挺常见的功能,就是用户登陆过一次后,系统会记住这个用户一段时间,
在这段时间内,用户不需要反复登陆就可以使用系统。
实现原理
image.png1、首次登陆后,cookie和用户信息会存到数据库
2、下一次登陆的时候,经过 RememberMeAuthenticationFilter ,拿着cookie去数据库查询用户
3、用2中返回的用户自动执行认认证逻辑
image.png
RememberMeAuthenticationFilter是最后一个绿色的过滤器,当所有过滤器都没法认证的时候,就使用RememberMeAuthenticationFilter进行认证。
案例2
第一步,页面
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<h2>标准登录页面</h2>
<h3>表单登录</h3>
<form action="/authentication/form" 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>图形验证码:</td>
<td>
<input type="text" name="imageCode">
<img src="/code/image?width=200">
</td>
</tr>
<tr>
<!--name名称是remember-me,固定不可改-->
<td colspan='2'><input name="remember-me" type="checkbox" value="true"/>记住我</td>
</tr>
<tr>
<td colspan="2">
<button type="submit">登录</button>
</td>
</tr>
</table>
</form>
</body>
</html>
第二步,配置 PersistentTokenRepository
@Autowired
private DataSource dataSource;
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
//启动的时候就初始化表,注意,就在第一次启动的时候执行,以后要注释掉
tokenRepository.setCreateTableOnStartup(true);
return tokenRepository;
}
第三步,配置UserDetailsService
@Autowired
private UserDetailsService userDetailsService;
第三步,配置各个组件
.rememberMe()
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(securityProperties.getBrowser().getRememberMeSeconds())
.userDetailsService(userDetailsService)
完整配置类:BrowserSecurityConfig
package com.imooc.security.browser;
import com.imooc.security.browser.authentication.ImoocAuthencationFailureHandler;
import com.imooc.security.browser.authentication.ImoocAuthencationSuccessHandler;
import com.imooc.security.core.properties.SecurityProperties;
import com.imooc.security.core.validate.code.ValidateCodeFilter;
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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import javax.sql.DataSource;
/**
* WebSecurityConfigurerAdapter是专门用于配置web安全应用的适配器
*
* @Author 张柳宁
* @Description
* @Date Create in 2018/3/21
* @Modified By:
*/
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SecurityProperties securityProperties;
@Autowired
private ImoocAuthencationSuccessHandler imoocAuthencationSuccessHandler;
@Autowired
private ImoocAuthencationFailureHandler imoocAuthencationFailureHandler;
@Autowired
private DataSource dataSource;
@Autowired
private UserDetailsService userDetailsService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
// tokenRepository.setCreateTableOnStartup(true);
return tokenRepository;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
validateCodeFilter.setAuthenticationFailureHandler(imoocAuthencationFailureHandler);
validateCodeFilter.setSecurityProperties(securityProperties);
validateCodeFilter.afterPropertiesSet();
http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
.formLogin()
.loginPage("/authentication/require")
.loginProcessingUrl("/authentication/form")
.successHandler(imoocAuthencationSuccessHandler)
.failureHandler(imoocAuthencationFailureHandler)
.and()
.rememberMe()
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(securityProperties.getBrowser().getRememberMeSeconds())
.userDetailsService(userDetailsService)
.and()
.authorizeRequests()
.antMatchers("/authentication/require"
, securityProperties.getBrowser().getLoginPage()
, "/code/image").permitAll()
.anyRequest()
.authenticated()
.and()
.csrf()
.disable();
}
}
第四步,验证
登陆认证后,数据库中会创建persistent_logins表,并添加一条认证记录。
然后即使关闭服务,重新启动,访问需要认证的服务,也不需要认证,因为会通过数据库中的认证信息自动认证。
短信验证登陆
-
开发短信验证码接口
image.png -
检验短信验证码并登陆
image.png -
重构代码
完整代码
添加好友,备注:来自简书,一起 好好学习,天天向上
微信号
网友评论