识别当前用户是否合法
即:你是谁
当前用户能访问的数据、页面权限
即:你能干什么
防止伪造身份
在imooc-security-demo
项目中,配置
默认其实就是true
的
我们在浏览器中输入 http://localhost:8080/hello
发现并没有访问到接口,而是弹出一个认证窗口
这个其实就是SpringSecurity默认行为搞的鬼
默认的用户名:user,密码在启动日志中能看到
我们在认证窗口中填写好认证信息,就访问到了接口
在引入了基于SpringBoot的SpringSecurity后,其默认行为会拦截保护所有对服务器的访问。
每个请求都需要用户名/密码
认证
这种由SpringSecurity
提供的默认校验方式叫做httpBasic
,
实际环境中基本没用,所以我们往往需要自定义
通过继承WebSecurityConfigurerAdapter
适配器类,实现我们自定义的SpringSecurity
配置逻辑
由HttpBasic
的弹窗认证,改为表单认证
我们进行自定义配置
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;
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// 设置认证方式
http.formLogin()
.and()
// 对请求进行授权
.authorizeRequests()
// 认证请求
.anyRequest()
// 都需要身份认证
.authenticated();
}
}
此时我们再次启动demo
项目并进行访问,发现还是弹窗认证
原因是因为demo
项目默认扫描的包并不包含BrowserSecurityConfig
我们对demo
项目的启动类进行些许改造
package com.imooc.security.demo;
import com.imooc.security.browser.BrowserSecurityConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @Author sherry
* @Description
* @Date Create in 2019-03-13
* @Modified By:
*/
@SpringBootApplication(scanBasePackageClasses = {DemoApplication.class, BrowserSecurityConfig.class})
@RestController
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class);
}
@GetMapping("/hello")
public String hello() {
return "Hello Spring Security";
}
}
此时,我们访问/hello
,认证界面为:
我们访问/hello
,SpringSecurity
发现这个接口需要认证,然后就自动跳转到了/login
,然后就跳转到了默认的表单登陆页面
我们输入默认的user
用户名和密码,就能够访问到/hello
接口了
默认对所有接口进行弹窗认证,其配置如下
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// 设置认证方式
http.httpBasic()
.and()
// 对请求进行授权
.authorizeRequests()
// 认证请求
.anyRequest()
// 都需要身份认证
.authenticated();
}
}
我们使用颜色对过滤器进行分类
绿色
绿色
的过滤器,代表一种认证方式,如UsernamePasswordAuthenticationFilter
,处理表单登陆
BasicAuthenticationFilter
,处理httpBasic
绿色的过滤器,检查当前的请求中,是否有当前过滤器所需的信息
UsernamePasswordAuthenticationFilter
:首先判断当前是不是登陆请求,然后判断这个请求中有没有带用户名/密码,如果带了,则进行校验,如果没带,则放行,走下一个过滤器
BasicAuthenticationFilter
:判断请求头中有没有Basic
开头的认证信息,有则认证,没有,则放行,走下一个过滤器
任何一个绿色的过滤器,认证成功后,会对当前请求做一个标记,表示认证成功了
注意,所有的绿色过滤器都会生效。然后根据我们的请求参数符合哪个过滤器,哪个过滤器就会处理本次认证
蓝色
蓝色
用于捕获橙色
抛出来的异常,当认证不通过
的时候,橙色是会抛出异常的
蓝色过滤器捕获到橙色过滤器的异常后,将异常传递给对应的绿色过滤器,进行认证不通过时的处理
如对于表单认证,就会跳转到表单登陆页面
橙色
橙色
的过滤器,过滤器链的最后一层,由其决定是否对当前请求放行。一旦放行,就能够访问到REST API
判断依据为代码中的配置,如:
就会要求所有的接口,必须被认证后才能访问
这个配置逻辑现在是比较简单的,事实上可以配置得非常复杂,如放行某些接口,某些接口只有某些客户才能访问等等
绿色
的过滤器是可以由我们控制是否生效,并且支持自定义的。另外两种类型的过滤器不是由我们控制的我们阅读下面几个类
UsernamePasswordAuthenticationFilter
负责表单登陆的过滤器
ExceptionTranslationFilter
处理认证过程中的异常
FilterSecurityInterceptor
判断认证是否通过
当我们直接访问API的时候,FilterSecurityInterceptor
检测到认证未通过,异常被ExceptionTranslationFilter`捕获到,根据我们的配置,跳转到默认的登陆页面上。
在登陆页面中输入了正确的用户名密码,被UsernamePasswordAuthenticationFilter
认证通过,最终FilterSecurityInterceptor
确认可以访问,遂成功访问到API
如何获取用户信息,在SpringSecurity
中,是被封装在UserDetailsService
接口中的
我们实现这个接口,将查询到的用户信息保存到UserDetails
对象中
SpringSecurity
会根据UserDetails
中的密码,与用户请求中填写的密码进行比较
UserDetailsService
:接口用于获取用户身份信息
UserDetails
:接口用于保存获取到的用户信息
org.springframework.security.core.userdetails.User
:是Spring Security
为我们提供的对UserDetails
的一个实现。
UserDetailsService
接口package com.imooc.security.browser;
import lombok.extern.slf4j.Slf4j;
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
@Slf4j
public class MyUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//模拟从数据库中获取用户信息
log.info("获取用户名:{} 的认证信息", username);
return new User(username, "123456", AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
username
为前端传递过来的用户名,我们从数据库中查询出这个用户的权限信息,
AuthorityUtils.commaSeparatedStringToAuthorityList("admin")
将角色字符串转化成所需的类型
使用User
对象封装后进行返回,Spring Security
会将返回的真实认证信息与用户输入的认证信息进行比较,如果不相符,
UserDetails
接口提供了4个方法,可以由我们自定义实现
方法 | 说明 |
---|---|
isAccountNonExpired | 用户是否未过期,true-未过期 |
isAccountNonLocked | 用户是否未锁定,冻结,true-未冻结 |
isCredentialsNonExpired | 密码是否未过期,true-未过期 |
isEnabled | 用户是否未可用,被删除,true-未被删除 |
关于isAccountNonLocked
与isEnabled
,一般在业务上,一个表示冻结,一个表示删除。 其实都只是两个状态,删除也只是打一个标记位,一般不会真的删除。 被冻结的一般可以被恢复,删除的就不恢复了。
在我们自己实现UserDetails
的时候,如果上述四个方法返回false
,就表示不通过
如果我们对用户的状态有这种需求,就可以自定义UserDetails
的实现
我们不会把密码的明文存到数据库中
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
加密过的密文才行了
SpringSecurity
将用户输入的密码,与返回的加密密码,通过调用matches
方法进行校验。
默认是跳转请求的url
默认是显示错误信息
在browser
项目中编写自定义登陆页面
<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;
/**
* @Author sherry
* @Description
* @Date Create in 2019-03-16
* @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")
// 配置处理登陆请求的url,即登陆页面提交的请求地址
.loginProcessingUrl("/authentication/form")
.and()
// 配置认证请求方式
.authorizeRequests()
// 跳转到登陆页面的请求不需要认证
.antMatchers("/imooc-signIn.html").permitAll()
// 所有的请求
.anyRequest()
// 都需要身份认证
.authenticated()
// 忽略CSRF认证,是Spring Boot Security默认配置的一个安全项(跨站请求防护),暂时关闭
.and()
.csrf().disable();
}
}
此时我们再访问接口,发现就是跳转到我们的自定义认证页面了
这里有一个注意点,一开始的时候,页面跳转的时候报了个404错。原因在于html页面并没有被打包。
修改根pom的resources配置,添加
即可
**/*.html
自定义任务页面完整代码
默认SpringSecurity
处理的表单登陆请求为/login
,在这个自定义页面中,我使用了自定义的/authentication/form
,所以需要在BrowserSecurityConfig
中进行配置.loginProcessingUrl("/authentication/form")
目前,当我们访问API
,检测到需要认证,自动跳转到了一个html
登陆页面,那么如果是一个app来访问我们呢?这么做就不是那么合理了。所以,我们下面做一个功能,通过用户的请求不同,决定跳转到html
登陆页,还是访问json
信息。
编写一个自定义Controller,判断是请求认证失败后,按照html处理还是按照json处理
package com.imooc.security.browser;
import com.imooc.security.browser.support.SimpleResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
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;
@RestController
@Slf4j
public class BrowserSecurityController {
//请求的缓存对象
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
log.info("引发跳转的URL:{}", target);
if (StringUtils.endsWithIgnoreCase(target, ".html")) {//如果引发跳转的url后缀为html,则跳转到html登陆页面
//跳转到自定义配置的登陆页面
redirectStrategy.sendRedirect(request, response, "/imooc-signIn.html");
}
}
return new SimpleResponse("访问的服务需要身份认证");
}
}
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;
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 设置认证方式
http.formLogin()
// 检测到未登录,跳转到登陆页面 或 处理跳转的Controller地址
.loginPage("/authentication/require")
// 配置处理登陆请求的url,即登陆页面提交的请求地址;默认为 /login
.loginProcessingUrl("/authentication/form")
.and()
// 配置认证请求方式
.authorizeRequests()
// 跳转到登陆页面的请求不需要认证
.antMatchers("/imooc-signIn.html", "/authentication/require").permitAll()
// 所有的请求
.anyRequest()
// 都需要身份认证
.authenticated()
// 忽略CSRF认证,是Spring Boot Security默认配置的一个安全项(跨站请求防护),暂时关闭
.and()
.csrf().disable();
}
}
对于一些可以灵活变化的信息,我们把它抽取到配置类中。如:认证失败后的登录页地址
配置类的编写一般遵循这些个原则
1、一个系统中不要有太多的配置类入口
2、java中提供默认配置
3、调用方的配置中间中可以覆盖2中的配置
4、请求的url参数可以覆盖3中的配置
这么做的话,我们的系统配置就非常灵活了
package com.imooc.security.core.properties;
public class BrowserProperties {
private String loginPage = "/imooc-signIn.html";
public String getLoginPage() {
return loginPage;
}
public void setLoginPage(String loginPage) {
this.loginPage = loginPage;
}
}
package com.imooc.security.core.properties;
import org.springframework.boot.context.properties.ConfigurationProperties;
@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;
}
}
package com.imooc.security.core;
import com.imooc.security.core.properties.SecurityProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableConfigurationProperties(SecurityProperties.class)//使配置文件生效
public class SecurityCoreConfig {
}
然后我们就可以修改我们的代码了
在demo
中配置
imooc:
security:
browser:
loginPage: /imooc-demo.html
访问:http://localhost:8080/hello.html
,发现页面被跳转到了我们配置的页面上,而不是原先的imooc-signIn.html
页面
默认情况下,登陆成功后,去访问原先请求的URL。但在实际场景中,我们在登陆成功后,往往需要做很多操作,如登陆日志的记录等等。
默认情况下,登陆失败后,是在登陆页面显示错误信息。如果我们还想有些额外的操作,该如何处理呢?
自定义认证成功处理类
package com.imooc.security.browser.auth;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.imooc.security.core.properties.SecurityProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
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;
@Component("imoocAuthSuccessHandler")
@Slf4j
public class ImoocAuthSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
private ObjectMapper objectMapper;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
log.info("登陆成功");
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
response.getWriter().write(objectMapper.writeValueAsString(authentication));
}
}
登陆认证成功后,我们往浏览器输出当前认证用户对象信息,如下:
"authenticated": true,
:当前信息已经经过了身份认证
"authorities": [
{
"authority": "admin"
}
],
用户拥有的权限,或者说是角色
"credentials": null,
:用户密码,但一般被SpringSecurity
处理后会隐藏起来
"details": {
"remoteAddress": "0:0:0:0:0:0:0:1",
"sessionId": "C86D521C7A80B54DB39B1B5FD135EED5"
},
认证请求的客户端信息
"name": "fdfd"
:认证请求的用户名
"principal": {
"password": null,
"username": "fdfd",
"authorities": [
{
"authority": "admin"
}
],
"accountNonExpired": true,
"accountNonLocked": true,
"credentialsNonExpired": true,
"enabled": true
},
我们自定义的UserDetails
中的内容
我们不再实现接口,而是继承SavedRequestAwareAuthenticationSuccessHandler
,这个是SpringSecurity
默认的成功处理器
package com.imooc.security.browser.auth;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.imooc.security.core.properties.LoginType;
import com.imooc.security.core.properties.SecurityProperties;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.builder.ReflectionToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
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;
@Component("imoocAuthSuccessHandler")
@Slf4j
public class ImoocAuthSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@Autowired
private ObjectMapper objectMapper;
@Autowired
private SecurityProperties securityProperties;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
log.info("登陆成功:{}", securityProperties.getBrowser().getLoginType());
if (LoginType.JSON == securityProperties.getBrowser().getLoginType()) {//如果当前系统配置的是json请求
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
// response.getWriter().write(objectMapper.writeValueAsString(authentication));
log.info(ReflectionToStringBuilder.toString(authentication, ToStringStyle.MULTI_LINE_STYLE));
//然后按照默认处理,继续调用请求的接口
super.onAuthenticationSuccess(request, response, authentication);
} else {//如果当前系统配置的不是json,则按照默认处理,默认处理请求的接口
// redirectStrategy.sendRedirect(request,response,"/index1.html");//跳转到自定义的页面上
super.onAuthenticationSuccess(request, response, authentication);
}
}
}
自定义认证失败处理类
package com.imooc.security.browser.auth;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.imooc.security.browser.support.SimpleResponse;
import com.imooc.security.core.properties.LoginType;
import com.imooc.security.core.properties.SecurityProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
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;
@Slf4j
@Component("/imoocAuthFailureHandler")
public class ImoocAuthFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Autowired
private ObjectMapper objectMapper;
@Autowired
private SecurityProperties securityProperties;
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
log.info("登陆失败:{}", securityProperties.getBrowser().getLoginType());
if (LoginType.JSON == securityProperties.getBrowser().getLoginType()) {
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
response.getWriter().write(objectMapper.writeValueAsString(new SimpleResponse(e.getMessage())));
} else {
//默认跳转到登陆认证页
super.onAuthenticationFailure(request, response, e);
}
}
}
我们的成功、失败处理器,只有配置后才会起作用
系统中往往会有这种需求,就是需要知道当前访问的人是谁
@GetMapping("/me")
public Object getCurrentUser(){
return SecurityContextHolder.getContext().getAuthentication();
}
@GetMapping("/me1")
public Object getCurrentUser(Authentication authentication){
return authentication;
}
SecurityContextPersistenceFilter
进来的时候检查session
中是否有认证信息,有,放到请求线程中;
出去的时候检查线程中是否有认证信息,有,放到session
中
package com.imooc.security.core.validate.code;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.awt.image.BufferedImage;
import java.time.LocalDateTime;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ImageCode {
// 图片
private BufferedImage image;
// 随机数
private String code;
// 过期时间点
private LocalDateTime expireTime;
/**
* @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);
}
}
package com.imooc.security.core.validate.code;
import lombok.extern.slf4j.Slf4j;
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.RequestMapping;
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;
@RestController
@RequestMapping("/code")
@Slf4j
public class ValidateCodeController {
public static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";
// 操作Session的Spring工具类
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
/**
* 验证码生成,并将验证码存储在session中
* @param request
* @param response
* @throws IOException
*/
@GetMapping("/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();
}
/**
* 生成图形验证码
*
* @param request
* @return
*/
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);
}
}
package com.imooc.security.core.validate.code;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
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;
@Slf4j
public class ValidateCodeFilter extends OncePerRequestFilter {
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;
}
}
OncePerRequestFilter:Spring提供的工具类,保证过滤器仅被调用一次
package com.imooc.security.core.validate.code;
import org.springframework.security.core.AuthenticationException;
public class ValidateCodeException extends AuthenticationException {
public ValidateCodeException(String message) {
super(message);
}
}
package com.imooc.security.browser;
import com.imooc.security.browser.auth.ImoocAuthFailureHandler;
import com.imooc.security.browser.auth.ImoocAuthSuccessHandler;
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;
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SecurityProperties securityProperties;
@Autowired
private ImoocAuthSuccessHandler imoocAuthSuccessHandler;
@Autowired
private ImoocAuthFailureHandler imoocAuthFailureHandler;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
validateCodeFilter.setAuthenticationFailureHandler(imoocAuthFailureHandler);
// 在UsernamePasswordAuthenticationFilter前添加自定义过滤器
http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
.formLogin()
// 检测到未登录,跳转到登陆页面 或 处理跳转的Controller地址
.loginPage("/authentication/require")
// 配置处理登陆请求的url,即登陆页面提交的请求地址;默认为 /login
.loginProcessingUrl("/authentication/form")
.successHandler(imoocAuthSuccessHandler)
.failureHandler(imoocAuthFailureHandler)
.and()
// 配置认证请求方式
.authorizeRequests()
// 跳转到登陆页面的请求不需要认证
.antMatchers(securityProperties.getBrowser().getLoginPage()
, "/authentication/require"
, "/code/image").permitAll()
// 所有的请求
.anyRequest()
// 都需要身份认证
.authenticated()
// 忽略CSRF认证,是Spring Boot Security默认配置的一个安全项(跨站请求防护),暂时关闭
.and()
.csrf().disable();
}
}
完整代码
package com.imooc.security.core.properties;
import lombok.Data;
/**
* @Author sherry
* @Description
* @Date Create in 2019-03-20
* @Modified By:
*/
@Data
public class ImageCodeProperties {
private int width = 67;
private int height = 23;
private int length = 4;
private int expireIn = 60;
}
package com.imooc.security.core.properties;
import lombok.Data;
/**
* @Author sherry
* @Description
* @Date Create in 2019-03-20
* @Modified By:
*/
@Data
public class ValidateCodeProperties {
private ImageCodeProperties image = new ImageCodeProperties();
}
package com.imooc.security.core.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "imooc.security")
@Data
public class SecurityProperties {
private BrowserProperties browser = new BrowserProperties();
private ValidateCodeProperties code = new ValidateCodeProperties();
}
package com.imooc.security.core;
import com.imooc.security.core.properties.SecurityProperties;
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 {
}
imooc:
security:
browser:
loginPage: /imooc-demo.html
loginType: REDIRECT
code:
image:
length: 6
width: 200
height: 20
<html>
<head>
<meta charset="UTF-8">
<title>登录-demotitle>
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&height=50">
td>
tr>
<tr>
<td colspan="2">
<button type="submit">登录button>
td>
tr>
table>
form>
body>
html>
package com.imooc.security.core.validate.code;
import com.imooc.security.core.properties.SecurityProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
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 sherry
* @Description
* @Date Create in 2019-03-18
* @Modified By:
*/
@RestController
@RequestMapping("/code")
@Slf4j
public class ValidateCodeController {
@Autowired
private SecurityProperties securityProperties;
public static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";
// 操作Session的Spring工具类
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
/**
* 验证码生成,并将验证码存储在session中
*
* @param request
* @param response
* @throws IOException
*/
@GetMapping("/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();
}
/**
* 生成图形验证码
*
* @param request
* @return
*/
private ImageCode createImageCode(HttpServletRequest request) {
int width = ServletRequestUtils.getIntParameter(request,"width",securityProperties.getCode().getImage().getWidth());
int height = ServletRequestUtils.getIntParameter(request,"height",securityProperties.getCode().getImage().getHeight());
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 < securityProperties.getCode().getImage().getLength(); 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();
log.info("生成验证码:{}",sRand);
return new ImageCode(image, sRand, securityProperties.getCode().getImage().getExpireIn());
}
/**
* 生成随机背景条纹
*
* @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);
}
}
imooc:
security:
browser:
loginPage: /imooc-demo.html
loginType: REDIRECT
code:
image:
length: 6
width: 200
height: 20
urls:
- /authentication/form
- /user
- /user/*
@Data
public class ImageCodeProperties {
private int width = 67;
private int height = 23;
private int length = 4;
private int expireIn = 60;
//需要验证码校验的接口
private List<String> urls;
}
package com.imooc.security.core.validate.code;
import com.imooc.security.core.properties.SecurityProperties;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
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.FilterConfig;
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;
/**
* @Author sherry
* @Description
* @Date Create in 2019-03-18
* @Modified By:
*/
@Slf4j
public class ValidateCodeFilter extends OncePerRequestFilter implements InitializingBean {
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
private AuthenticationFailureHandler authenticationFailureHandler;
private SecurityProperties securityProperties;
private AntPathMatcher antPathMatcher;
private Set<String> urls;
@Override
public void afterPropertiesSet() throws ServletException {
urls = new HashSet<>(securityProperties.getCode().getImage().getUrls());
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
boolean action = false;
for (String url : urls) {
if (antPathMatcher.match(url, request.getRequestURI())) {
log.info("对:{} 执行图像验证码校验", 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 void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
this.authenticationFailureHandler = authenticationFailureHandler;
}
public void setSecurityProperties(SecurityProperties securityProperties) {
this.securityProperties = securityProperties;
}
public void setAntPathMatcher(AntPathMatcher antPathMatcher) {
this.antPathMatcher = antPathMatcher;
}
}
package com.imooc.security.browser;
import com.imooc.security.browser.auth.ImoocAuthFailureHandler;
import com.imooc.security.browser.auth.ImoocAuthSuccessHandler;
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;
import org.springframework.util.AntPathMatcher;
/**
* @Author sherry
* @Description
* @Date Create in 2019-03-16
* @Modified By:
*/
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SecurityProperties securityProperties;
@Autowired
private ImoocAuthSuccessHandler imoocAuthSuccessHandler;
@Autowired
private ImoocAuthFailureHandler imoocAuthFailureHandler;
@Autowired
private AntPathMatcher antPathMatcher;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AntPathMatcher antPathMatcher() {
return new AntPathMatcher();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
validateCodeFilter.setAuthenticationFailureHandler(imoocAuthFailureHandler);
validateCodeFilter.setSecurityProperties(securityProperties);
validateCodeFilter.setAntPathMatcher(antPathMatcher);
// 调用初始化方法
validateCodeFilter.afterPropertiesSet();
// 在UsernamePasswordAuthenticationFilter前添加自定义过滤器
http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
.formLogin()
// 检测到未登录,跳转到登陆页面 或 处理跳转的Controller地址
.loginPage("/authentication/require")
// 配置处理登陆请求的url,即登陆页面提交的请求地址;默认为 /login
.loginProcessingUrl("/authentication/form")
.successHandler(imoocAuthSuccessHandler)
.failureHandler(imoocAuthFailureHandler)
.and()
// 配置认证请求方式
.authorizeRequests()
// 跳转到登陆页面的请求不需要认证
.antMatchers(securityProperties.getBrowser().getLoginPage()
, "/authentication/require"
, "/code/image").permitAll()
// 所有的请求
.anyRequest()
// 都需要身份认证
.authenticated()
// 忽略CSRF认证,是Spring Boot Security默认配置的一个安全项(跨站请求防护),暂时关闭
.and()
.csrf().disable();
// 设置认证方式
}
}
所谓验证码生成逻辑可配置,就是说,提供接口,供使用方进行实现
package com.imooc.security.core.validate.code;
import javax.servlet.ServletRequest;
public interface ValidateCodeGenerator {
ImageCode generate(ServletRequest request);
}
package com.imooc.security.core.validate.code;
import com.imooc.security.core.properties.SecurityProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.ServletRequestUtils;
import javax.servlet.ServletRequest;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.Random;
@Slf4j
public class ImageCodeGenerator implements ValidateCodeGenerator {
private SecurityProperties securityProperties;
@Override
public ImageCode generate(ServletRequest request) {
int width = ServletRequestUtils.getIntParameter(request, "width", securityProperties.getCode().getImage().getWidth());
int height = ServletRequestUtils.getIntParameter(request, "height", securityProperties.getCode().getImage().getHeight());
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 < securityProperties.getCode().getImage().getLength(); 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();
log.info("生成验证码:{}", sRand);
return new ImageCode(image, sRand, securityProperties.getCode().getImage().getExpireIn());
}
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);
}
public void setSecurityProperties(SecurityProperties securityProperties) {
this.securityProperties = securityProperties;
}
}
package com.imooc.security.core.validate.code;
import com.imooc.security.core.properties.SecurityProperties;
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;
/**
* @Author sherry
* @Description
* @Date Create in 2019-03-20
* @Modified By:
*/
@Configuration
public class ValidateCodeBeanConfig {
@Autowired
private SecurityProperties securityProperties;
@Bean
// 如果用户没有实现接口,且名称为 imageCodeGenerator,则使用系统默认提供的
@ConditionalOnMissingBean(name = "imageCodeGenerator")
public ValidateCodeGenerator imageCodeGenerator() {
ValidateCodeGenerator codeGenerator = new ImageCodeGenerator();
((ImageCodeGenerator) codeGenerator).setSecurityProperties(securityProperties);
return codeGenerator;
}
}
package com.imooc.security.core.validate.code;
import com.imooc.security.core.properties.SecurityProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
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.RequestMapping;
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.io.IOException;
@RestController
@RequestMapping("/code")
@Slf4j
public class ValidateCodeController {
public static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";
// 操作Session的Spring工具类
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@Autowired
private ValidateCodeGenerator imageCodeGenerator;
/**
* 验证码生成,并将验证码存储在session中
*
* @param request
* @param response
* @throws IOException
*/
@GetMapping("/image")
public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
ImageCode imageCode = imageCodeGenerator.generate(request);
sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY, imageCode);
ServletOutputStream sos = response.getOutputStream();
ImageIO.write(imageCode.getImage(), "JPEG", sos);
sos.close();
}
}
如果我们自定义了一个bean名称为imageCodeGenerator
的类,就会替换掉系统的默认配置
形如
@Component("imageCodeGenerator")
public MyImageCodeGenerator implements ValidateCodeGenerator {
...
}
这里体现了一个开发思想,就是以增量的方式与适应变化。就是说当需求、逻辑发生变动的时候,是增加代码,而不是去修改原先的代码
完整代码
这个也是身份认证的一个常见特性
即:用户登录认证成功后,在一段时间内不需要重复认证
1、首次登陆后,cookie
和用户信息会存到数据库
2、下一次登陆的时候,经过 RememberMeAuthenticationFilter
,拿着cookie
去数据库查询用户
3、用2中返回的用户自动执行认认证逻辑
RememberMeAuthenticationFilter
是最后一个绿色的过滤器,当所有过滤器都没法认证的时候,就使用RememberMeAuthenticationFilter
进行认证。
<html>
<head>
<meta charset="UTF-8">
<title>登录-demotitle>
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">
td>
tr>
<tr>
<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
在BrowserSecurityConfig
中添加配置
@Autowired
private DataSource dataSource;
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
//启动的时候就初始化表,注意,就在第一次启动的时候执行,以后要注释掉
tokenRepository.setCreateTableOnStartup(true);
return tokenRepository;
}
待会儿修改配置的时候要用到这个Bean
UserDetailsService
@Autowired
private UserDetailsService userDetailsService;
也是在BrowserSecurityConfig
中添加配置
BrowserSecurityConfig
中整合各个组件package com.imooc.security.browser;
import com.imooc.security.browser.auth.ImoocAuthFailureHandler;
import com.imooc.security.browser.auth.ImoocAuthSuccessHandler;
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 org.springframework.util.AntPathMatcher;
import javax.sql.DataSource;
/**
* @Author sherry
* @Description
* @Date Create in 2019-03-16
* @Modified By:
*/
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SecurityProperties securityProperties;
@Autowired
private ImoocAuthSuccessHandler imoocAuthSuccessHandler;
@Autowired
private ImoocAuthFailureHandler imoocAuthFailureHandler;
@Autowired
private AntPathMatcher antPathMatcher;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AntPathMatcher antPathMatcher() {
return new AntPathMatcher();
}
@Autowired
private DataSource dataSource;
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
//启动的时候就初始化表,注意,就在第一次启动的时候执行,以后要注释掉
tokenRepository.setCreateTableOnStartup(true);
return tokenRepository;
}
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
validateCodeFilter.setAuthenticationFailureHandler(imoocAuthFailureHandler);
validateCodeFilter.setSecurityProperties(securityProperties);
validateCodeFilter.setAntPathMatcher(antPathMatcher);
// 调用初始化方法
validateCodeFilter.afterPropertiesSet();
// 在UsernamePasswordAuthenticationFilter前添加自定义过滤器
http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
.formLogin()
// 检测到未登录,跳转到登陆页面 或 处理跳转的Controller地址
.loginPage("/authentication/require")
// 配置处理登陆请求的url,即登陆页面提交的请求地址;默认为 /login
.loginProcessingUrl("/authentication/form")
.successHandler(imoocAuthSuccessHandler)
.failureHandler(imoocAuthFailureHandler)
.and()
.rememberMe()
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(securityProperties.getBrowser().getRememberMeSeconds())
.userDetailsService(userDetailsService)
.and()
// 配置认证请求方式
.authorizeRequests()
// 跳转到登陆页面的请求不需要认证
.antMatchers(securityProperties.getBrowser().getLoginPage()
, "/authentication/require"
, "/code/image").permitAll()
// 所有的请求
.anyRequest()
// 都需要身份认证
.authenticated()
// 忽略CSRF认证,是Spring Boot Security默认配置的一个安全项(跨站请求防护),暂时关闭
.and()
.csrf().disable();
// 设置认证方式
}
}
登陆认证后,数据库中会创建persistent_logins表,并添加一条认证记录。
然后即使关闭服务,重新启动,访问需要认证的服务,也不需要认证,因为会通过数据库中的认证信息自动认证。
一般我们是自己创建表结构
create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, "
+ "token varchar(64) not null, last_used timestamp not null)
除了用户名密码登录外,手机号+短信验证码登录也是一种常见的登陆方式
用户名密码登陆过程中,由SpringSecurity
提供的过滤器实现认证,手机号+短信的认证方式中,我们只验证手机,验证码在更前面的验证码过滤器中进行校验。
我们已经开发了一个图形验证码接口,我们就在此基础上进行开发
其实逻辑和开发图形验证码是类似的
1、生成
2、存入session
3、告知使用者
像这种主干逻辑相同,但是步骤实现不同,我们可以使用模板方法的模式抽象出来
详细代码
从ValidateCodeController
作为入口来看
具体逻辑以代码为主
主要是代码重构的技巧,不是SpringSecurity
的内容,这里就不赘述了
不同的认证方式需要由不同的过滤器来处理,对于用户名密码形式的表单认证,系统已经提供了对应的过滤器。
下面,我们就需要根据仿照这个流程,对短信验证码的登陆形式进行一次认证。实现一些自定义的类
注意,本流程只是对手机号进行校验,不是对手机号和短信验证码进行校验。。 短信验证码的校验放在更前面的过滤器中。走一遍这个流程,是为了让用户处于已认证状态。之所以短信验证码单独提取出来,是因为短信验证码、图形验证码,这个都是比较通用的功能,在系统的好多地方都能够用到。
SmsCodeAuthenticationToken
是用户身份对象
package com.imooc.security.core.auth.mobile;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import java.util.Collection;
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = 420L;
private final Object principal;
public SmsCodeAuthenticationToken(String mobile) {
super((Collection)null);
this.principal = mobile;
this.setAuthenticated(false);
}
public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
super.setAuthenticated(true);
}
public Object getCredentials() {
return null;
}
public Object getPrincipal() {
return this.principal;
}
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if(isAuthenticated) {
throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
} else {
super.setAuthenticated(false);
}
}
public void eraseCredentials() {
super.eraseCredentials();
}
}
这个类参考UsernamePasswordAuthenticationToken
编写
拦截短信验证登录请求,并初始化SmsCodeAuthenticationToken
package com.imooc.security.core.auth.mobile;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public static final String IMOOC_FORM_MOBILE_KEY = "mobile";
private String mobileParameter = IMOOC_FORM_MOBILE_KEY;
private boolean postOnly = true;
public SmsCodeAuthenticationFilter() {
//校验请求的url与方法类型
super(new AntPathRequestMatcher("/authentication/mobile", "POST"));
}
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if(this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
String mobile = this.obtainMobile(request);
if(mobile == null) {
mobile = "";
}
mobile = mobile.trim();
SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
/**
* 获取手机号的方法
* @param request
* @return
*/
protected String obtainMobile(HttpServletRequest request) {
return request.getParameter(this.mobileParameter);
}
protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
public String getMobileParameter() {
return mobileParameter;
}
public void setMobileParameter(String mobileParameter) {
this.mobileParameter = mobileParameter;
}
}
提供校验逻辑
package com.imooc.security.core.auth.mobile;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
private UserDetailsService userDetailsService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;
// (String) authenticationToken.getPrincipal(),这个获取到的是手机号,通过手机号获取用户信息
UserDetails user = userDetailsService.loadUserByUsername((String) authenticationToken.getPrincipal());
if(user==null){
throw new InternalAuthenticationServiceException("无法获取用户信息");
}
//如果还有其他校验逻辑的话,是要加载这里的,如,手机号和短信验证码是否匹配。当然,现在是把这个校验放在了最前面的过滤器中了
// 认证后的 SmsCodeAuthenticationToken
SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(user,user.getAuthorities());
//把未认证的信息放到已认证的detail里面
authenticationResult.setDetails(authenticationToken.getDetails());
return authenticationResult;
}
@Override
public boolean supports(Class<?> authentication) {
return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
}
public UserDetailsService getUserDetailsService() {
return userDetailsService;
}
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
}
短信验证码过滤器
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 SmsCodeFilter 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().getSms().getUrl(), ",");
// 初始化当前配置的需要验证码的url
// for (String config : configUrls) {
// urls.add(config);
// }
urls.add("/authentication/mobile");//登陆是必须要验证码的
}
@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) {
ValidateCode codeInSession = (ValidateCode) sessionStrategy.getAttribute(request, ValidateCodeProcessor.SESSION_KEY_PREFIX+"SMS");
String codeInRequest = "";
try {
codeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(), "smsCode");
} catch (ServletRequestBindingException e) {
throw new ValidateCodeException("获取验证码的值失败");
}
if (StringUtils.isBlank(codeInRequest)) {
throw new ValidateCodeException("验证码的值不能为空");
}
if (codeInSession == null) {
throw new ValidateCodeException("验证码不存在");
}
if (codeInSession.isExpired()) {
sessionStrategy.removeAttribute(request, ValidateCodeProcessor.SESSION_KEY_PREFIX+"SMS");
throw new ValidateCodeException("验证码已过期");
}
if (!StringUtils.equals(codeInSession.getCode(), codeInRequest)) {
throw new ValidateCodeException("验证码不匹配");
}
sessionStrategy.removeAttribute(request, ValidateCodeProcessor.SESSION_KEY_PREFIX+"SMS");
}
public AuthenticationFailureHandler getAuthenticationFailureHandler() {
return authenticationFailureHandler;
}
public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
this.authenticationFailureHandler = authenticationFailureHandler;
}
public Set<String> getUrls() {
return urls;
}
public void setUrls(Set<String> urls) {
this.urls = urls;
}
public SecurityProperties getSecurityProperties() {
return securityProperties;
}
public void setSecurityProperties(SecurityProperties securityProperties) {
this.securityProperties = securityProperties;
}
}
package com.imooc.security.core.authentication.mobile;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.stereotype.Component;
@Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain,HttpSecurity> {
@Autowired
private AuthenticationSuccessHandler imoocAuthenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler imoocAuthenticationFailureHandler;
@Autowired
private UserDetailsService userDetailsService;
@Override
public void configure(HttpSecurity http) {
SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(imoocAuthenticationSuccessHandler);
smsCodeAuthenticationFilter.setAuthenticationFailureHandler(imoocAuthenticationFailureHandler);
SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);
http.authenticationProvider(smsCodeAuthenticationProvider)
.addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
短信验证码与图形验证码的过滤器有很多重复部分,做一个重构。
表单认证完整代码