源碼地址:https://github.com/springsecuritydemo/microservice-auth-center04
通過(guò)之前文章的學(xué)習(xí),我們已經(jīng)基本上掌握了SpringSecurity的基本流程楚午。你會(huì)發(fā)現(xiàn)夕晓,真正的login請(qǐng)求時(shí)有SpringSecurity幫我們處理的菱蔬,那么我們?nèi)绾螌?shí)現(xiàn)自定義表單登錄呢,必須添加一個(gè)驗(yàn)證碼等。
一宠页、添加驗(yàn)證碼
我們這里為了方便,直接從百度找了個(gè)生成驗(yàn)證碼的代碼寇仓,你也可以使用自己項(xiàng)目中的驗(yàn)證碼生成工具举户。
1.1 生成驗(yàn)證碼工具類(lèi)
public class VerifyCodeUtils {
//使用到Algerian字體,系統(tǒng)里沒(méi)有的話需要安裝字體遍烦,字體只顯示大寫(xiě)俭嘁,去掉了1,0,i,o幾個(gè)容易混淆的字符
public static final String VERIFY_CODES = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
private static Random random = new Random();
/**
* 使用系統(tǒng)默認(rèn)字符源生成驗(yàn)證碼
* @param verifySize 驗(yàn)證碼長(zhǎng)度
* @return
*/
public static String generateVerifyCode(int verifySize){
return generateVerifyCode(verifySize, VERIFY_CODES);
}
/**
* 使用指定源生成驗(yàn)證碼
* @param verifySize 驗(yàn)證碼長(zhǎng)度
* @param sources 驗(yàn)證碼字符源
* @return
*/
public static String generateVerifyCode(int verifySize, String sources){
if(sources == null || sources.length() == 0){
sources = VERIFY_CODES;
}
int codesLen = sources.length();
Random rand = new Random(System.currentTimeMillis());
StringBuilder verifyCode = new StringBuilder(verifySize);
for(int i = 0; i < verifySize; i++){
verifyCode.append(sources.charAt(rand.nextInt(codesLen-1)));
}
return verifyCode.toString();
}
/**
* 輸出指定驗(yàn)證碼圖片流
* @param w
* @param h
* @param os
* @param code
* @throws IOException
*/
public static void outputImage(int w, int h, OutputStream os, String code) throws IOException{
int verifySize = code.length();
BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
Random rand = new Random();
Graphics2D g2 = image.createGraphics();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
Color[] colors = new Color[5];
Color[] colorSpaces = new Color[] { Color.WHITE, Color.CYAN,
Color.GRAY, Color.LIGHT_GRAY, Color.MAGENTA, Color.ORANGE,
Color.PINK, Color.YELLOW };
float[] fractions = new float[colors.length];
for(int i = 0; i < colors.length; i++){
colors[i] = colorSpaces[rand.nextInt(colorSpaces.length)];
fractions[i] = rand.nextFloat();
}
Arrays.sort(fractions);
g2.setColor(Color.GRAY);// 設(shè)置邊框色
g2.fillRect(0, 0, w, h);
Color c = getRandColor(200, 250);
g2.setColor(c);// 設(shè)置背景色
g2.fillRect(0, 2, w, h-4);
//繪制干擾線
Random random = new Random();
g2.setColor(getRandColor(160, 200));// 設(shè)置線條的顏色
for (int i = 0; i < 20; i++) {
int x = random.nextInt(w - 1);
int y = random.nextInt(h - 1);
int xl = random.nextInt(6) + 1;
int yl = random.nextInt(12) + 1;
g2.drawLine(x, y, x + xl + 40, y + yl + 20);
}
// 添加噪點(diǎn)
float yawpRate = 0.05f;// 噪聲率
int area = (int) (yawpRate * w * h);
for (int i = 0; i < area; i++) {
int x = random.nextInt(w);
int y = random.nextInt(h);
int rgb = getRandomIntColor();
image.setRGB(x, y, rgb);
}
shear(g2, w, h, c);// 使圖片扭曲
g2.setColor(getRandColor(100, 160));
int fontSize = h-4;
Font font = new Font("Algerian", Font.ITALIC, fontSize);
g2.setFont(font);
char[] chars = code.toCharArray();
for(int i = 0; i < verifySize; i++){
AffineTransform affine = new AffineTransform();
affine.setToRotation(Math.PI / 4 * rand.nextDouble() * (rand.nextBoolean() ? 1 : -1), (w / verifySize) * i + fontSize/2, h/2);
g2.setTransform(affine);
g2.drawChars(chars, i, 1, ((w-10) / verifySize) * i + 5, h/2 + fontSize/2 - 10);
}
g2.dispose();
ImageIO.write(image, "jpg", os);
}
private static Color getRandColor(int fc, int bc) {
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);
}
private static int getRandomIntColor() {
int[] rgb = getRandomRgb();
int color = 0;
for (int c : rgb) {
color = color << 8;
color = color | c;
}
return color;
}
private static int[] getRandomRgb() {
int[] rgb = new int[3];
for (int i = 0; i < 3; i++) {
rgb[i] = random.nextInt(255);
}
return rgb;
}
private static void shear(Graphics g, int w1, int h1, Color color) {
shearX(g, w1, h1, color);
shearY(g, w1, h1, color);
}
private static void shearX(Graphics g, int w1, int h1, Color color) {
int period = random.nextInt(2);
boolean borderGap = true;
int frames = 1;
int phase = random.nextInt(2);
for (int i = 0; i < h1; i++) {
double d = (double) (period >> 1)
* Math.sin((double) i / (double) period
+ (6.2831853071795862D * (double) phase)
/ (double) frames);
g.copyArea(0, i, w1, 1, (int) d, 0);
if (borderGap) {
g.setColor(color);
g.drawLine((int) d, i, 0, i);
g.drawLine((int) d + w1, i, w1, i);
}
}
}
private static void shearY(Graphics g, int w1, int h1, Color color) {
int period = random.nextInt(40) + 10; // 50;
boolean borderGap = true;
int frames = 20;
int phase = 7;
for (int i = 0; i < w1; i++) {
double d = (double) (period >> 1)
* Math.sin((double) i / (double) period
+ (6.2831853071795862D * (double) phase)
/ (double) frames);
g.copyArea(i, 0, 1, h1, 0, (int) d);
if (borderGap) {
g.setColor(color);
g.drawLine(i, (int) d, i, 0);
g.drawLine(i, (int) d + h1, i, h1);
}
}
}
}
1.2 編寫(xiě)Redis配置、封裝結(jié)果集和生成圖片接口
我們將生成的驗(yàn)證碼存入到服務(wù)器的 Session 對(duì)象中服猪,但如果你的項(xiàng)目是分布式項(xiàng)目或者是App項(xiàng)目供填,這里就不能存入到Session中,可以考慮使用 Redis 存儲(chǔ)罢猪。我們采用Redis 存儲(chǔ)方案近她。
添加redis依賴包:
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
添加redis和圖片驗(yàn)證碼失效時(shí)間:
...
redis:
#數(shù)據(jù)庫(kù)索引
database: 0
host: 127.0.0.1
port: 6379
password:
#連接超時(shí)時(shí)間
timeout: 5000
loginCode:
expiration: 1 #登錄驗(yàn)證碼過(guò)期時(shí)間,單位 分鐘
prefix: login_code #驗(yàn)證碼redis的key值前綴
編寫(xiě)圖片結(jié)果集:
@Data
@AllArgsConstructor
public class ImgResult {
private String img;
private String uuid;
}
編寫(xiě)獲取驗(yàn)證碼接口:
// 登錄驗(yàn)證碼過(guò)期時(shí)間:?jiǎn)挝?分鐘
@Value("${loginCode.expiration}")
private Long expiration;
@Value("${loginCode.prefix}")
private String prefix;
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 獲取驗(yàn)證碼
*/
@GetMapping("/vCode")
@ResponseBody
public ImgResult getCode() throws IOException {
// 生成隨機(jī)字串
String verifyCode = VerifyCodeUtils.generateVerifyCode(4);
String uuid = UUID.randomUUID().toString();
// 存入redis
redisTemplate.opsForValue().set(prefix + uuid,verifyCode, expiration, TimeUnit.MINUTES);
// 生成圖片
int w = 111, h = 36;
ByteArrayOutputStream stream = new ByteArrayOutputStream();
VerifyCodeUtils.outputImage(w, h, stream, verifyCode);
try {
return new ImgResult(Base64.encode(stream.toByteArray()),uuid);
} catch (Exception e) {
e.printStackTrace();
return null;
} finally {
stream.close();
}
}
這里采用 Base64
格式的圖片返回膳帕,使用Hutool
依賴包完成Base64轉(zhuǎn)換:
<!--工具包-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>4.5.11</version>
</dependency>
1.3 修改login.html
在原來(lái)的 login 頁(yè)面集成上加入 驗(yàn)證碼字段:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登陸</title>
<script src="js/jquery-3.4.1.min.js"></script>
</head>
<body>
<h1>登陸</h1>
<form method="post" action="/login">
<div>
用戶名:<input type="text" name="username">
</div>
<div>
密 碼:<input type="password" name="password">
</div>
<div>
驗(yàn)證碼:<input type="text" class="form-control" name="verifyCode" required="required" placeholder="驗(yàn)證碼">
<input id="uuid" type="hidden" name="uuid" />
<img id="vCode" title="看不清粘捎,請(qǐng)點(diǎn)我" onclick="getVerifyCode()" onmouseover="mouseover(this)" />
</div>
<div>
<label><input type="checkbox" name="remember-me"/>自動(dòng)登錄</label>
</div>
<div>
<button type="submit">立即登陸</button>
</div>
</form>
<script>
$(function() {
getVerifyCode();
})
function getVerifyCode() {
var url = "/vCode?" + Math.random();
$.ajax({
//請(qǐng)求方式
type : "GET",
//請(qǐng)求的媒體類(lèi)型
contentType: "application/json;charset=UTF-8",
//請(qǐng)求地址
url : url,
//請(qǐng)求成功
success : function(result) {
console.log(result);
$("#uuid").val(result.uuid);
$("#vCode").attr("src","data:image/png;base64," + result.img);
},
//請(qǐng)求失敗,包含具體的錯(cuò)誤信息
error : function(e){
console.log(e.status);
console.log(e.responseText);
}
});
}
function mouseover(obj) {
obj.style.cursor = "pointer";
}
</script>
</body>
</html>
1.4 添加匿名訪問(wèn) URL(放行 驗(yàn)證碼請(qǐng)求)
在 WebSecurityConfig
中允許 驗(yàn)證碼請(qǐng)求匿名訪問(wèn)危彩,不然沒(méi)有登錄就沒(méi)辦法獲取驗(yàn)證碼(死循環(huán)了)攒磨。
@Autowired
private StringRedisTemplate redisTemplate;
@Value("${loginCode.prefix}")
private String prefix;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 如果有允許匿名的url,填在下面
.antMatchers("/vCode").permitAll()
.anyRequest().authenticated()
.and()
// 設(shè)置登陸頁(yè)
.formLogin().loginPage("/login")
// 設(shè)置登陸成功url
.defaultSuccessUrl("/").permitAll()
// 設(shè)置登錄失敗url
.failureUrl("/login/error")
// 自定義登陸用戶名和密碼參數(shù)恬砂,默認(rèn)為username和password
// .usernameParameter("username")
// .passwordParameter("password")
.and()
// 添加圖片驗(yàn)證碼過(guò)濾器
.addFilterBefore(new VerifyFilter(redisTemplate, prefix), UsernamePasswordAuthenticationFilter.class)
.logout().permitAll()
// 自動(dòng)登錄
.and().rememberMe()
.tokenRepository(persistentTokenRepository())
// 有效時(shí)間咧纠,單位:s
.tokenValiditySeconds(60)
.userDetailsService(userDetailsService);
// 關(guān)閉CSRF跨域
http.csrf().disable();
}
這樣驗(yàn)證碼就加好了。
1.5 運(yùn)行程序
二泻骤、驗(yàn)證碼驗(yàn)證
驗(yàn)證方式:
- AJAX 驗(yàn)證
- 過(guò)濾器驗(yàn)證
-
Spring Security 驗(yàn)證
接下來(lái)我們分別針對(duì)這幾種驗(yàn)證方式做講解漆羔。
2.1 AJAX驗(yàn)證
使用 AJAX 方式驗(yàn)證和我們 Spring Security 框架就沒(méi)有任何關(guān)系了,其實(shí)就是表單提交前先發(fā)個(gè) HTTP 請(qǐng)求驗(yàn)證驗(yàn)證碼狱掂,本篇不再贅述演痒。有興趣的同學(xué)可以自己實(shí)現(xiàn)。
2.2 過(guò)濾器驗(yàn)證
使用過(guò)濾器驗(yàn)證的思路: 在SpringSecurity 處理登錄驗(yàn)證請(qǐng)求前趋惨,先驗(yàn)證驗(yàn)證碼鸟顺,如果正確,放行;如果不正確讯嫂,拋出異常蹦锋。
具體實(shí)現(xiàn)步驟如下:
第一步:編寫(xiě)自定義驗(yàn)證碼異常,繼承AuthenticationException
抽象類(lèi)
public class VerifyCodeException extends AuthenticationException {
public VerifyCodeException(String msg) {
super(msg);
}
public VerifyCodeException(String msg, Throwable t) {
super(msg, t);
}
}
第二步:編寫(xiě)驗(yàn)證碼過(guò)濾器
自定義一個(gè)過(guò)濾器欧芽,實(shí)現(xiàn) OncePerRequestFilter
(用于防止多次執(zhí)行Filter的莉掂;也就是說(shuō)一次請(qǐng)求只會(huì)走一次攔截器鏈) ,在 isProtectedUrl()
方法中攔截 POST 方式的/login
請(qǐng)求千扔。
在邏輯處理中從 request 中取出驗(yàn)證碼憎妙,并進(jìn)行驗(yàn)證,如果驗(yàn)證成功曲楚,放行厘唾;驗(yàn)證失敗,手動(dòng)拋出異常龙誊。
public class VerifyFilter extends OncePerRequestFilter{
private static final PathMatcher pathMatcher = new AntPathMatcher();
private StringRedisTemplate stringRedisTemplate;
private String prefix;
public VerifyFilter() {}
public VerifyFilter(StringRedisTemplate stringRedisTemplate, String prefix) {
this.stringRedisTemplate = stringRedisTemplate;
this.prefix = prefix;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 攔截 /login的POST請(qǐng)求
if(isProtectedUrl(request)) {
String uuid = request.getParameter("uuid"); // 圖片驗(yàn)證碼的 key
String vCode = request.getParameter("verifyCode");// 圖片驗(yàn)證碼的 value
if(!validateVerify(request, uuid, vCode)) {
//手動(dòng)設(shè)置異常
request.getSession().setAttribute("SPRING_SECURITY_LAST_EXCEPTION",new VerifyCodeException("驗(yàn)證碼輸入錯(cuò)誤"));
// 轉(zhuǎn)發(fā)到錯(cuò)誤Url
request.getRequestDispatcher("/login/error").forward(request,response);
} else {
filterChain.doFilter(request,response);
}
} else {
filterChain.doFilter(request,response);
}
}
/**
* 驗(yàn)證驗(yàn)證碼合法性
* @param uuid 驗(yàn)證key
* @param vCode 驗(yàn)證值
* @return
*/
private boolean validateVerify(HttpServletRequest request, String uuid, String vCode) {
// 查詢驗(yàn)證碼
String code = stringRedisTemplate.opsForValue().get(prefix + uuid);
// 清除驗(yàn)證碼
stringRedisTemplate.delete(prefix + uuid);
if (StringUtils.isBlank(code)) {
//手動(dòng)設(shè)置異常
request.getSession().setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, new VerifyCodeException("驗(yàn)證碼已過(guò)期"));
return false;
}
if (StringUtils.isBlank(vCode) || !vCode.equalsIgnoreCase(code)) {
request.getSession().setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, new VerifyCodeException("驗(yàn)證碼錯(cuò)誤"));
return false;
}
logger.info("驗(yàn)證碼:" + code + "用戶輸入:" + vCode);
return true;
}
// 攔截 /login的POST請(qǐng)求
private boolean isProtectedUrl(HttpServletRequest request) {
return "POST".equals(request.getMethod()) && pathMatcher.match("/login", request.getServletPath());
}
}
第三步:修改loginError()
方法抚垃,添加 圖片驗(yàn)證碼異常處理 :
特別注意:這里不要指定請(qǐng)求方式,而使用:@RequestMapping("/login/error")
载迄,這里之前我做這個(gè)測(cè)試讯柔,發(fā)現(xiàn)SpringSecurity 默認(rèn)錯(cuò)誤調(diào)整使用的是 GET 方式,這里我們手動(dòng)通過(guò) request.getDispatcher("/login/error").forward()
使用的是 post方式护昧。所以這里一定記得修改注解方式為 @RequestMapping()
魂迄,不然你會(huì)發(fā)現(xiàn)怎么也不成功。
@RequestMapping("/login/error")
@ResponseBody
public Result loginError(HttpServletRequest request) {
AuthenticationException authenticationException = (AuthenticationException) request.getSession().getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
log.info("authenticationException={}", authenticationException);
Result result = new Result();
result.setCode(201);
// 圖片驗(yàn)證碼校驗(yàn)
if(authenticationException instanceof VerifyCodeException) {
result.setMsg(authenticationException.getMessage());
} else if (authenticationException instanceof UsernameNotFoundException || authenticationException instanceof BadCredentialsException) {
result.setMsg("用戶名或密碼錯(cuò)誤");
} else if (authenticationException instanceof DisabledException) {
result.setMsg("用戶已被禁用");
} else if (authenticationException instanceof LockedException) {
result.setMsg("賬戶被鎖定");
} else if (authenticationException instanceof AccountExpiredException) {
result.setMsg("賬戶過(guò)期");
} else if (authenticationException instanceof CredentialsExpiredException) {
result.setMsg("證書(shū)過(guò)期");
} else {
result.setMsg("登錄失敗");
}
return result;
}
第四步:注入過(guò)濾器
修改 WebSecurityConfig
中 configure()
方法惋耙,添加一個(gè) addFilterBefore()
捣炬,具有兩個(gè)參數(shù),作用是在參數(shù)二之前執(zhí)行參數(shù)一指定的過(guò)濾器绽榛。
SpringSecurity 對(duì)于用戶名/密碼登錄方式是通過(guò) UsernamePasswordAuthenticationFilter
處理的湿酸,所以我們?cè)谒皥?zhí)行自定義驗(yàn)證碼過(guò)濾器即可。
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 如果有允許匿名的url灭美,填在下面
.antMatchers("/vCode").permitAll()
.anyRequest().authenticated()
.and()
// 設(shè)置登陸頁(yè)
.formLogin().loginPage("/login")
// 設(shè)置登陸成功url
.defaultSuccessUrl("/").permitAll()
// 設(shè)置登錄失敗url
.failureUrl("/login/error")
// 自定義登陸用戶名和密碼參數(shù)推溃,默認(rèn)為username和password
// .usernameParameter("username")
// .passwordParameter("password")
.and()
// 添加圖片驗(yàn)證碼過(guò)濾器
.addFilterBefore(new VerifyFilter(redisTemplate), UsernamePasswordAuthenticationFilter.class)
.logout().permitAll()
// 自動(dòng)登錄
.and().rememberMe()
.tokenRepository(persistentTokenRepository())
// 有效時(shí)間,單位:s
.tokenValiditySeconds(60)
.userDetailsService(userDetailsService);
// 關(guān)閉CSRF跨域
http.csrf().disable();
第五步:運(yùn)行程序
上面我們使用過(guò)濾器實(shí)現(xiàn)了驗(yàn)證功能届腐,但是其他它和AJAX 驗(yàn)證差別不大铁坎。
- AJAZ 驗(yàn)證是在登錄提交前發(fā)送一個(gè)異步請(qǐng)求,請(qǐng)求返回成功就提交登錄犁苏;失敗就不提交登錄硬萍。
- 過(guò)濾器是先驗(yàn)證驗(yàn)證碼,驗(yàn)證成功就讓 SpringSecurity 驗(yàn)證用戶名和密碼围详;驗(yàn)證失敗則拋出異常朴乖。
如果我們要做的需求是用戶登錄時(shí)需要多個(gè)驗(yàn)證字段,不單單是用戶名和密碼,那么使用過(guò)濾器會(huì)讓邏輯變得復(fù)雜买羞,而這里我們通過(guò)另外一種方式來(lái)完整驗(yàn)證邏輯袁勺。
2.3 SpringSecurity驗(yàn)證
第一步:自定義 WebAuthenticationDetails
類(lèi):
我們知道SpringSecurity 默認(rèn)情況下只會(huì)處理用戶名和密碼信息。
WebAuthenticationDetails
: 該類(lèi)提供了獲取用戶登錄時(shí)攜帶的額外信息的功能畜普,默認(rèn)提供了 remoteAddress 與 sessionId 信息魁兼。
public class WebAuthenticationDetails implements Serializable {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
// ~ Instance fields
// ================================================================================================
private final String remoteAddress;
private final String sessionId;
...
這時(shí)候我們就要自定義 CustomWebAuthenticationDetails
類(lèi),并在其中加入我們的驗(yàn)證碼字段:
package com.thtf.auth.security;
import org.springframework.security.core.SpringSecurityCoreVersion;
import org.springframework.security.web.authentication.WebAuthenticationDetails;
import javax.servlet.http.HttpServletRequest;
/**
* ========================
* 獲取用戶登錄時(shí)攜帶的額外信息
* Created with IntelliJ IDEA.
* User:pyy
* Date:2019/7/24 16:50
* Version: v1.0
* ========================
*/
public class CustomWebAuthenticationDetails extends WebAuthenticationDetails {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private final String uuid; //驗(yàn)證碼key
private final String verifyCode; //驗(yàn)證碼value
public CustomWebAuthenticationDetails(HttpServletRequest request) {
super(request);
this.uuid = request.getParameter("uuid");
this.verifyCode = request.getParameter("verifyCode");
}
public String getUuid() {
return uuid;
}
public String getVerifyCode() {
return verifyCode;
}
}
在這個(gè)類(lèi)我們?cè)黾觾蓚€(gè)屬性:uuid 和 verifyCode漠嵌。
第二步:配置 AuthenticationDetailsSource
自定義了 WebAuthenticationDetails
,我們需要將其放入到 AuthenticationDetailsSource
中替換原來(lái)的 WebAuthenticationDetails
對(duì)象盖呼,所以我們還得實(shí)現(xiàn)自定義 AuthenticationDetailsSource
:
package com.thtf.auth.security;
import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.web.authentication.WebAuthenticationDetails;
import javax.servlet.http.HttpServletRequest;
/**
* ========================
* 該接口用于在Spring Security登錄過(guò)程中對(duì)用戶的登錄信息的詳細(xì)信息進(jìn)行填充
* Created with IntelliJ IDEA.
* User:pyy
* Date:2019/7/24 17:06
* Version: v1.0
* ========================
*/
@Component("authenticationDetailsSource")
public class CustomAuthenticationDetailsSource implements AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> {
@Override
public WebAuthenticationDetails buildDetails(HttpServletRequest request) {
return new CustomWebAuthenticationDetails(request);
}
}
第三步:將 CustomAuthenticationDetailsSource
注入到SpringSecurity中儒鹿。
修改 WebSecurityConfig
,在 configure()
方法中使用 authenticationDetailsSource(authenticationDetailsSource)
方法來(lái)指定它几晤,替換默認(rèn)的AuthenticationDetailsSource
對(duì)象约炎。
@Autowired
private AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> authenticationDetailsSource;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 如果有允許匿名的url,填在下面
.antMatchers("/vCode").permitAll()
.anyRequest().authenticated()
.and()
// 設(shè)置登陸頁(yè)
.formLogin().loginPage("/login")
// 設(shè)置登陸成功url
.defaultSuccessUrl("/").permitAll()
// 設(shè)置登錄失敗url
.failureUrl("/login/error")
// 自定義登陸用戶名和密碼參數(shù)蟹瘾,默認(rèn)為username和password
// .usernameParameter("username")
// .passwordParameter("password")
// 指定authenticationDetailsSource
.authenticationDetailsSource(authenticationDetailsSource)
.and()
// 添加圖片驗(yàn)證碼過(guò)濾器
//.addFilterBefore(new VerifyFilter(redisTemplate, prefix), UsernamePasswordAuthenticationFilter.class)
.logout().permitAll()
// 自動(dòng)登錄
.and().rememberMe()
.tokenRepository(persistentTokenRepository())
// 有效時(shí)間圾浅,單位:s
.tokenValiditySeconds(60)
.userDetailsService(userDetailsService);
// 關(guān)閉CSRF跨域
http.csrf().disable();
}
第四步:自定義 AuthenticationProvider
上面我們通過(guò)自定義 WebAuthenticationDetails
和AuthenticationDetailsSource
將驗(yàn)證碼key、驗(yàn)證碼值和用戶名憾朴、密碼一起帶入了Spring Security中狸捕,下面我們需要將它取出來(lái)。
這里需要我們自定義AuthenticationProvider
众雷,需要注意:如果是我們自己實(shí)現(xiàn)AuthenticationProvider
灸拍,那么我們就需要自己做密碼校驗(yàn)了。
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
@Autowired
private CustomUserDetailsService customUserDetailsService;
@Autowired
private StringRedisTemplate redisTemplate;
@Value("${loginCode.prefix}")
private String prefix;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 獲取用戶輸入的用戶名和密碼
String username = authentication.getName();
String password = authentication.getCredentials().toString();
CustomWebAuthenticationDetails details = (CustomWebAuthenticationDetails) authentication.getDetails();
String uuid = details.getUuid();
String vCode = details.getVerifyCode();
// 查詢驗(yàn)證碼
String code = redisTemplate.opsForValue().get(prefix + uuid);
// 清除驗(yàn)證碼
redisTemplate.delete(prefix + uuid);
if (StringUtils.isBlank(code)) {
throw new VerifyCodeException("驗(yàn)證碼已過(guò)期");
}
if (StringUtils.isBlank(vCode) || !vCode.equalsIgnoreCase(code)) {
throw new VerifyCodeException("驗(yàn)證碼錯(cuò)誤");
}
// userDetails為數(shù)據(jù)庫(kù)中查詢到的用戶信息
UserDetails userDetails = customUserDetailsService.loadUserByUsername(username);
// 如果是自定義AuthenticationProvider砾省,需要手動(dòng)密碼校驗(yàn)
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
if(!bCryptPasswordEncoder.matches(password, userDetails.getPassword())) {
throw new BadCredentialsException("密碼錯(cuò)誤");
}
return new UsernamePasswordAuthenticationToken(username, password, userDetails.getAuthorities());
}
@Override
public boolean supports(Class<?> authentication) {
// 這里不要忘記鸡岗,和UsernamePasswordAuthenticationToken比較
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
第五步: 在 WebSecurityConfig 中注入 CustomAuthenticationProvider
:
@Autowired
private CustomAuthenticationProvider customAuthenticationProvider;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
auth.authenticationProvider(customAuthenticationProvider);
}
第六步:運(yùn)行程序
是不是更復(fù)雜了O(∩_∩)O哈哈~