Security系列教程
- ①Security入门体验
- ②Security自定义账号密码验证+thymeleaf登录案例(附带网页案例
- ③Security+验证码登录案例(附带网页案例)
- ④Security+jwt 实现无状态认证,前后端分离(附带网页案例))
简介
本章主要实现Spring Security自定义用户认证功能,并使用thymeleaf
编写一个简单网页供测试使用。
- Springboot版本2.5.3
- 整合网页模板引擎
thymeleaf
hutool
工具包生成验证码
1. 环境
- 基础环境搭建请参考:②Security自定义账号密码验证+thymeleaf登录案例(附带网页案例
pom.xml
中新增hutool
工具包,用于生成验证码
<!-- hutool工具包 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.2</version>
</dependency>
2. 生成验证码
- 我们封装一个验证码校验工具类,我们的思路是:把验证码存入
session
会话中,需要校验时再从session
会话中取出进行校验。 - 首先封装一个
RequestUtils
工具,用于静态获取session
和request
。
/**
* 请求工具类
*
* @author ding
*/
public class RequestUtils {
/**
* 获取session
*/
public static HttpSession getHttpSession() {
return getHttpRequest().getSession();
}
/**
* 获取request
*/
public static HttpServletRequest getHttpRequest() {
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
assert servletRequestAttributes != null;
return servletRequestAttributes.getRequest();
}
}
- 验证码生成工具
CaptchaUtils
/**
* 生成及校验图片验证码
*
* @author ding
*/
public class CaptchaUtils {
private static final String CAPTCHA = "captcha";
/**
* CircleCaptcha 圆圈干扰验证码
* 定义图形验证码的长、宽、验证码字符数、干扰元素个数
*/
public static void getCircleCaptcha(HttpSession session, HttpServletResponse response) throws IOException {
LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(200, 100, 5, 20);
session.setAttribute(CAPTCHA, lineCaptcha.getCode());
writeResp(lineCaptcha, response);
}
/**
* 验证码校验
*
* @param code 验证码
*/
public static boolean verify(String code) {
HttpSession session = RequestUtils.getHttpSession();
String captcha = (String) session.getAttribute(CAPTCHA);
return code.equals(captcha);
}
/**
* http图片响应
*/
private static void writeResp(AbstractCaptcha abstractCaptcha, HttpServletResponse response) throws IOException {
ServletOutputStream out = null;
try {
out = response.getOutputStream();
abstractCaptcha.write(out);
} finally {
if (Objects.nonNull(out)) {
out.close();
}
}
}
}
- 编写一个验证码获取接口
@GetMapping("/getCode")
@ResponseBody
public void getCode(HttpSession session, HttpServletResponse response) throws IOException {
CaptchaUtils.getCircleCaptcha(session, response);
}
- 验证码获取并不需要认证,所以我们在
WebSecurityConfig
中开放该请求。 - 以下为
WebSecurityConfig
部分截取
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.headers().frameOptions().disable();//开启运行iframe嵌套页面
// 1、配置权限认证
http.authorizeRequests()
// 1. 配置不拦截路由
.antMatchers("/toLogin", "/getCode").permitAll()
// 任何其它请求
.anyRequest()
// 都需要身份认证
.authenticated()
略...
- 改造网页,新增验证码获取
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>登陆页</title>
</head>
<body>
<div>
<form action="/login" method="post">
<h2>登陆页</h2>
<h6>账号:admin,密码:123456</h6>
<!-- 验证码获取 -->
<img style="width: auto;height: 100px" src="/getCode"/>
<br />
<label for="username">账号:</label><input style="margin-left: 17px" type="text" id="username" name="username" placeholder="admin">
<br />
<label for="password">密码:</label><input style="margin-left: 17px" type="password" id="password" name="password" placeholder="123456">
<br />
<label for="code">验证码:</label><input type="text" id="code" name="code" placeholder="">
<br />
<br />
<button type="submit">登陆</button>
<div th:if="${param.captchaInvalid}" style="color: red;">
验证码错误.
</div>
<div th:if="${param.error}" style="color: red;">
账号或密码错误.
</div>
<div th:if="${param.logout}" style="color: red;">
登录失效.
</div>
</form>
</div>
</body>
</html>
- 启动项目,效果如下
3. 验证码校验
- 我们定义一个异常类,用于处理验证校验错误,重写
AuthenticationException
,定义我们的验证码类异常。
/**
* 验证码错误异常
*
* @author ding
*/
public class CaptchaInvalidException extends AuthenticationException {
public CaptchaInvalidException(String msg, Throwable cause) {
super(msg, cause);
}
public CaptchaInvalidException(String msg) {
super(msg);
}
}
- 接下来我们需要在
UserDetailsServiceImpl
中添加我们的校验逻辑,我们定义了一个code参数用来接收登录的验证码,由于security的默认登录接口只有一个用户名参数,那么我们如何获取验证码呢?
- 其实很简单,还记得我们开头封装的
RequestUtils
工具类吗,没错,我们可以通过静态获取request
的方式获取请求参数,如下:
@Component
@RequiredArgsConstructor
@Slf4j
public class UserDetailsServiceImpl implements UserDetailsService {
/**
* 模拟一个数据库用户
* 账号 admin
* 密码 123456
*/
private final static HashMap<String, MyUser> USER_MAP = new LinkedHashMap<>() {
{
put("admin", new MyUser()
.setUserId(1L)
.setUsername("admin")
.setPassword(new BCryptPasswordEncoder().encode("123456"))
);
}
};
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
HttpServletRequest request = RequestUtils.getHttpRequest();
String code = request.getParameter("code");
log.info("登录者账号为:{},验证码:code:{}", username, code);
// 验证码是否正确
if (!CaptchaUtils.verify(code)) {
throw new CaptchaInvalidException("验证码错误");
}
// 通过userName获取用户信息,实战中把USER_MAP换成数据库获取即可
MyUser user = USER_MAP.get(username);
if (user == null) {
throw new UsernameNotFoundException("not found");
}
// 角色和权限都在这里添加,角色以ROLE_前缀,不是ROLE_前缀的视为权限,这里添加了ROLE_ADMIN角色和read、write权限
List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_ADMIN,read,write");
AuthUser authUser = new AuthUser(user.getUsername(), user.getPassword(), authorities);
// 我们存放我们自定义的信息,如用户id,方便后续获取用户信息
authUser.setUserId(user.getUserId());
return authUser;
}
}
-
可以看见我们在校验完验证码后,抛出了一个
CaptchaInvalidException
异常,这是我们之前定义的一个专门处理验证码错误的异常,这么做的目的是为了在后续处理中,我们可以根据业务,给web端返回不同的呈现信息。 -
重新编写登录失败处理器
@Component
@Slf4j
public class LoginFailure extends SimpleUrlAuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException ex) throws IOException {
log.info("登录失败");
ex.printStackTrace();
this.saveException(request, ex);
if ("验证码错误".equals(ex.getMessage())) {
this.getRedirectStrategy().sendRedirect(request, response, "/toLogin?captchaInvalid=true");
} else {
this.getRedirectStrategy().sendRedirect(request, response, "/toLogin?error=true");
}
}
}
4. 最终效果演示
5. 源码分享
本系列项目已收录
Springboot、SpringCloud全家桶教程+源码,各种常用框架使用案例都有哦,具备完善的文档,致力于让开发者快速搭建基础环境并让应用跑起来,并提供丰富的使用示例供使用者参考,快来看看吧。