六、自定义认证案例
6.1 传统web开发认证总结案例
-
创建一个spring-security-03模块 -
导入依赖pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
<version>3.0.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.22</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.8</version>
</dependency>
</dependencies>
application.yml 配置文件
# 端口号
server:
port: 3034
# 服务应用名称
spring:
application:
name: SpringSecurity02
# 关闭thymeleaf缓存(用于修改完之后立即生效)
thymeleaf:
cache: false
# thymeleaf默认配置
prefix: classpath:/templates/
suffix: .html
encoding: UTF-8
mode: HTML
# 数据源
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/spring?useSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: 123456
mybatis:
# 注意 mapper 映射文件必须使用"/"
type-aliases-package: com.vinjcent.pojo
mapper-locations: com/vinjcent/mapper*.xml
# 日志处理,为了展示 mybatis 运行 sql 语句
logging:
level:
com:
vinjcent:
debug
- 编写实体类User、Role
package com.vinjcent.pojo;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.*;
public class User implements UserDetails {
private Integer id;
private String username;
private String password;
private boolean enabled;
private boolean accountNonExpired;
private boolean accountNonLocked;
private boolean credentialsNonExpired;
private List<Role> roles = new ArrayList<>();
}
package com.vinjcent.pojo;
import java.io.Serializable;
public class Role implements Serializable {
private Integer id;
private String name;
private String nameZh;
}
- 编写mapper、service、xml文件(这里只写接口,看接口实现方法)
package com.vinjcent.mapper;
import com.vinjcent.pojo.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
@Mapper
@Repository
public interface UserMapper {
User queryUserByUsername(@Param("username") String username);
}
package com.vinjcent.mapper;
import com.vinjcent.pojo.Role;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
@Mapper
@Repository
public interface RoleMapper {
List<Role> queryRolesByUid(@Param("uid") Integer uid);
}
- 自定义 DivUserDetailsService 实现 UserDetailsService,作为数据源进行身份认证
package com.vinjcent.config.security;
import com.vinjcent.pojo.Role;
import com.vinjcent.pojo.User;
import com.vinjcent.service.RoleService;
import com.vinjcent.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
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;
import org.springframework.util.ObjectUtils;
import java.util.List;
@Component
public class DivUserDetailsService implements UserDetailsService {
private final UserService userService;
private final RoleService roleService;
@Autowired
public DivUserDetailsService(UserService userService, RoleService roleService) {
this.userService = userService;
this.roleService = roleService;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userService.queryUserByUsername(username);
if (ObjectUtils.isEmpty(user)) throw new UsernameNotFoundException("用户名不正确!");
List<Role> roles = roleService.queryRolesByUid(user.getId());
user.setRoles(roles);
return user;
}
}
- 配置类 WebMvcConfigurer、WebSecurityConfigurerAdapter
package com.vinjcent.config.web;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class DivWebMvcConfigurer implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/toLogin").setViewName("login");
registry.addViewController("/index").setViewName("index");
registry.addViewController("/toIndex").setViewName("index");
}
}
- WebSecurityConfigurerAdapter
package com.vinjcent.config.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import javax.annotation.Resource;
@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
private final DivUserDetailsService userDetailsService;
@Autowired
public WebSecurityConfiguration(DivUserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.mvcMatchers("/toLogin").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/toLogin")
.loginProcessingUrl("/login")
.usernameParameter("uname")
.passwordParameter("passwd")
.defaultSuccessUrl("/toIndex", true)
.failureUrl("/toLogin")
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/toLogin")
.and()
.csrf()
.disable();
}
}
- html页面视图
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<meta charset="UTF-8">
<title>系统主页</title>
</head>
<body>
<h1>欢迎<span sec:authentication="principal.username"></span>,进入我的主页!</h1>
<hr>
<h1>获取认证用户信息</h1>
<ul>
<li sec:authentication="principal.username"></li>
<li sec:authentication="principal.authorities"></li>
<li sec:authentication="principal.accountNonExpired"></li>
<li sec:authentication="principal.accountNonLocked"></li>
<li sec:authentication="principal.credentialsNonExpired"></li>
</ul>
<a th:href="@{/logout}">退出登录</a>
</body>
</html>
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>登录页面</title>
</head>
<body>
<h1>用户登录</h1>
<form th:action="@{/login}" method="post">
用户名: <input type="text" name="uname"> <br>
密码: <input type="password" name="passwd"> <br>
<input type="submit" value="登录">
</form>
<h3>
<div th:text="${session.SPRING_SECURITY_LAST_EXCEPTION}"></div>
</h3>
</body>
</html>
- 根据数据库数据进行测试

6.2 前后端分离认证总结案例
根据前面章节的分析,发现在 Security 进行认证的时候,走的是 UsernamePasswordAuthenticationFilter 过滤器,并且调用的方法是 attemptAuthentication() 方法,并返回 Authentication 对象。传统 web 的认证方式并不满足前后端分离使用 json 数据格式进行交互,我们需要对认证用户信息的这个过滤器进行重写


-
创建一个模块 spring-security-04-separate -
导入依赖pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.22</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.8</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
application.yml 配置文件
server:
port: 3033
spring:
application:
name: SpringSecurity04-separate
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/spring?useSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: 123456
mybatis:
type-aliases-package: com.vinjcent.pojo
mapper-locations: com/vinjcent/mapper/**/*.xml
logging:
level:
com:
vinjcent:
debug
- 编写实体类User、Role
package com.vinjcent.pojo;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.*;
public class User implements UserDetails {
private Integer id;
private String username;
private String password;
private boolean enabled;
private boolean accountNonExpired;
private boolean accountNonLocked;
private boolean credentialsNonExpired;
private List<Role> roles = new ArrayList<>();
}
package com.vinjcent.pojo;
import java.io.Serializable;
public class Role implements Serializable {
private Integer id;
private String name;
private String nameZh;
}
- 编写mapper、service、xml文件(这里只写接口,看接口实现方法)
package com.vinjcent.mapper;
import com.vinjcent.pojo.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
@Mapper
@Repository
public interface UserMapper {
User queryUserByUsername(@Param("username") String username);
}
package com.vinjcent.mapper;
import com.vinjcent.pojo.Role;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
@Mapper
@Repository
public interface RoleMapper {
List<Role> queryRolesByUid(@Param("uid") Integer uid);
}
- 自定义 UserDetailsService 实现 UserDetailsService,作为数据源认证身份
package com.vinjcent.config.security;
import com.vinjcent.pojo.Role;
import com.vinjcent.pojo.User;
import com.vinjcent.service.RoleService;
import com.vinjcent.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
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;
import org.springframework.util.ObjectUtils;
import java.util.List;
@Component
public class DivUserDetailsService implements UserDetailsService {
private final UserService userService;
private final RoleService roleService;
@Autowired
public DivUserDetailsService(UserService userService, RoleService roleService) {
this.userService = userService;
this.roleService = roleService;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userService.queryUserByUsername(username);
if (ObjectUtils.isEmpty(user)) throw new UsernameNotFoundException("用户名不正确!");
List<Role> roles = roleService.queryRolesByUid(user.getId());
user.setRoles(roles);
return user;
}
}
- 编写 LoginFilter 继承 UsernamePasswordAuthenticationFilter 过滤器类
package com.vinjcent.filter;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
private boolean postOnly = true;
@Override
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());
}
if (request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)) {
try {
Map<String, String> userInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);
String username = userInfo.get(getUsernameParameter());
String password = userInfo.get(getPasswordParameter());
System.out.println("用户名: " + username + " 密码: " + password);
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);
setDetails(request, token);
return this.getAuthenticationManager().authenticate(token);
} catch (IOException e) {
e.printStackTrace();
}
}
return super.attemptAuthentication(request, response);
}
@Override
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
}
- 自定义认证成功、认证失败、退出登录处理事件
package com.vinjcent.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class DivAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
Map<String, Object> result = new HashMap<>();
result.put("msg","登陆成功");
result.put("status", 200);
result.put("用户信息", authentication.getPrincipal());
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpStatus.OK.value());
String info = new ObjectMapper().writeValueAsString(result);
response.getWriter().println(info);
}
}
package com.vinjcent.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class DivAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
Map<String, Object> result = new HashMap<>();
result.put("msg", "登陆失败: " + exception.getMessage());
result.put("status", 500);
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
String info = new ObjectMapper().writeValueAsString(result);
response.getWriter().println(info);
}
}
package com.vinjcent.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class DivLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
Map<String, Object> result = new HashMap<>();
result.put("msg","注销成功,当前认证对象为:" + authentication);
result.put("status", 200);
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpStatus.OK.value());
String info = new ObjectMapper().writeValueAsString(result);
response.getWriter().println(info);
}
}
- 编写配置类 WebSecurityConfiguration 继承 WebSecurityConfigurerAdapter
package com.vinjcent.config.security;
import com.vinjcent.filter.LoginFilter;
import com.vinjcent.handler.DivAuthenticationFailureHandler;
import com.vinjcent.handler.DivAuthenticationSuccessHandler;
import com.vinjcent.handler.DivLogoutSuccessHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
private final DivUserDetailsService userDetailsService;
@Autowired
public WebSecurityConfiguration(DivUserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
@Override
@Bean
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Bean
public LoginFilter loginFilter() throws Exception {
LoginFilter loginFilter = new LoginFilter();
loginFilter.setFilterProcessesUrl("/login");
loginFilter.setUsernameParameter("uname");
loginFilter.setPasswordParameter("passwd");
loginFilter.setAuthenticationManager(authenticationManager());
loginFilter.setAuthenticationSuccessHandler(new DivAuthenticationSuccessHandler());
loginFilter.setAuthenticationFailureHandler(new DivAuthenticationFailureHandler());
return loginFilter;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessHandler(new DivLogoutSuccessHandler())
.and()
.exceptionHandling()
.authenticationEntryPoint(((req, resp, ex) -> {
resp.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
resp.setStatus(HttpStatus.UNAUTHORIZED.value());
resp.getWriter().println("请认证之后再操作!");
}))
.and()
.csrf()
.disable();
http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
- 配置一个测试接口,作为请求资源进行测试
package com.vinjcent.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
@RequestMapping("/test")
public String test() {
return "Hello SpringSecurity!";
}
}




6.3 传统 web 开发之添加验证码
在 6.1 传统web开发认证总结案例开发基础上进行修改
- 添加依赖
pom.xml
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>
- 添加验证码配置类 KaptchaConfiguration
package com.vinjcent.config.verification;
import com.google.code.kaptcha.Producer;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Properties;
@Configuration
public class KaptchaConfiguration {
@Bean
public Producer kaptcha() {
Properties properties = new Properties();
properties.setProperty("kaptcha.image.width", "150");
properties.setProperty("kaptcha.image.height", "50");
properties.setProperty("kaptcha.textproducer.char.string", "0123456789");
properties.setProperty("kaptcha.textproducer.char.length", "4");
Config config = new Config(properties);
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}
- 返回图片处理结果并存储在 session 中,编写 controller
package com.vinjcent.controller;
import com.google.code.kaptcha.Producer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.awt.image.BufferedImage;
import java.io.IOException;
@Controller
public class VerifyCodeController {
private final Producer producer;
@Autowired
public VerifyCodeController(Producer producer) {
this.producer = producer;
}
@RequestMapping("/vc.jpg")
public void verifyCode(HttpServletResponse response, HttpSession session) throws IOException {
String verifyCode = producer.createText();
session.setAttribute("kaptcha", verifyCode);
BufferedImage image = producer.createImage(verifyCode);
response.setContentType("image/png#pic_center =800x");
ServletOutputStream os = response.getOutputStream();
ImageIO.write(image, "jpg", os);
}
}
- 自定义登录验和证码过滤器
package com.vinjcent.filter;
import com.vinjcent.exception.KaptchaNotMatchException;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.util.ObjectUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class LoginKaptchaFilter extends UsernamePasswordAuthenticationFilter {
private boolean postOnly = true;
public static final String SPRING_SECURITY_FORM_KAPTCHA = "kaptcha";
private String kaptchaParameter = SPRING_SECURITY_FORM_KAPTCHA;
@Override
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());
}
String verifyCode = request.getParameter(getKaptchaParameter());
String sessionVerifyCode = (String) request.getSession().getAttribute("kaptcha");
if (!ObjectUtils.isEmpty(verifyCode) && !ObjectUtils.isEmpty(sessionVerifyCode) && verifyCode.equals(sessionVerifyCode)) {
return super.attemptAuthentication(request, response);
}
throw new KaptchaNotMatchException("验证码不匹配!");
}
@Override
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
public String getKaptchaParameter() {
return kaptchaParameter;
}
public void setKaptchaParameter(String kaptchaParameter) {
this.kaptchaParameter = kaptchaParameter;
}
}
- 自定义验证码不匹配异常
package com.vinjcent.exception;
import org.springframework.security.core.AuthenticationException;
public class KaptchaNotMatchException extends AuthenticationException {
public KaptchaNotMatchException(String msg, Throwable cause) {
super(msg, cause);
}
public KaptchaNotMatchException(String msg) {
super(msg);
}
}
- 修改 WebSecurityConfiguration 配置类,将自定义的过滤器进行替换
package com.vinjcent.config.security;
import com.vinjcent.filter.LoginKaptchaFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
private final DivUserDetailsService userDetailsService;
@Autowired
public WebSecurityConfiguration(DivUserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
@Override
@Bean
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Bean
public LoginKaptchaFilter loginKaptchaFilter() throws Exception {
LoginKaptchaFilter loginKaptchaFilter = new LoginKaptchaFilter();
loginKaptchaFilter.setUsernameParameter("uname");
loginKaptchaFilter.setPasswordParameter("passwd");
loginKaptchaFilter.setKaptchaParameter("kaptcha");
loginKaptchaFilter.setAuthenticationManager(authenticationManager());
loginKaptchaFilter.setFilterProcessesUrl("/login");
loginKaptchaFilter.setAuthenticationSuccessHandler(((request, response, authentication) -> {
response.sendRedirect("/index");
}));
loginKaptchaFilter.setAuthenticationFailureHandler(((request, response, exception) -> {
response.sendRedirect("/toLogin");
}));
return loginKaptchaFilter;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.mvcMatchers("/toLogin").permitAll()
.mvcMatchers("/vc.jpg").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/toLogin")
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/toLogin")
.and()
.csrf()
.disable();
http.addFilterAt(loginKaptchaFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
- 运行测试
6.4 前后端分离开发之添加验证码
在 6.2 传统web开发认证总结案例开发基础上进行修改
- 添加依赖
pom.xml
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>
- 添加验证码配置类 KaptchaConfiguration
package com.vinjcent.config.verification;
import com.google.code.kaptcha.Producer;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Properties;
@Configuration
public class KaptchaConfiguration {
@Bean
public Producer kaptcha() {
Properties properties = new Properties();
properties.setProperty("kaptcha.image.width", "150");
properties.setProperty("kaptcha.image.height", "50");
properties.setProperty("kaptcha.textproducer.char.string", "0123456789");
properties.setProperty("kaptcha.textproducer.char.length", "4");
Config config = new Config(properties);
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}
- 返回图片处理结果并存储在 session 中,编写 controller(这里跟传统 web 开发有点不同,需要将图片转为Base64编码格式)
package com.vinjcent.controller;
import com.google.code.kaptcha.Producer;
import org.apache.tomcat.util.codec.binary.Base64;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.FastByteArrayOutputStream;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.awt.image.BufferedImage;
import java.io.IOException;
@RestController
public class VerifyCodeController {
private final Producer producer;
@Autowired
public VerifyCodeController(Producer producer) {
this.producer = producer;
}
@RequestMapping("/vc.jpg")
public String verifyCode(HttpSession session) throws IOException {
String verifyCode = producer.createText();
session.setAttribute("kaptcha", verifyCode);
BufferedImage image = producer.createImage(verifyCode);
FastByteArrayOutputStream fos = new FastByteArrayOutputStream();
ImageIO.write(image, "jpg", fos);
return Base64.encodeBase64String(fos.toByteArray());
}
}
- 自定义登录验和证码过滤器
package com.vinjcent.filter;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.vinjcent.exception.KaptchaNotMatchException;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.util.ObjectUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
public class LoginKaptchaFilter extends UsernamePasswordAuthenticationFilter {
private boolean postOnly = true;
public static final String SPRING_SECURITY_FORM_KAPTCHA = "kaptcha";
private String kaptchaParameter = SPRING_SECURITY_FORM_KAPTCHA;
@Override
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());
}
try {
Map<String, String> userInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);
String username = userInfo.get(getUsernameParameter());
String password = userInfo.get(getPasswordParameter());
String verifyCode = userInfo.get(getKaptchaParameter());
String sessionVerifyCode = (String) request.getSession().getAttribute("kaptcha");
if (!ObjectUtils.isEmpty(verifyCode) && !ObjectUtils.isEmpty(sessionVerifyCode) && verifyCode.equals(sessionVerifyCode)) {
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
throw new KaptchaNotMatchException("验证码不匹配!");
} catch (IOException e) {
e.printStackTrace();
}
String verifyCode = request.getParameter(getKaptchaParameter());
String sessionVerifyCode = (String) request.getSession().getAttribute("kaptcha");
if (!ObjectUtils.isEmpty(verifyCode) && !ObjectUtils.isEmpty(sessionVerifyCode) && verifyCode.equals(sessionVerifyCode)) {
return super.attemptAuthentication(request, response);
}
throw new KaptchaNotMatchException("验证码不匹配!");
}
@Override
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
public String getKaptchaParameter() {
return kaptchaParameter;
}
public void setKaptchaParameter(String kaptchaParameter) {
this.kaptchaParameter = kaptchaParameter;
}
}
- 自定义验证码不匹配异常
package com.vinjcent.exception;
import org.springframework.security.core.AuthenticationException;
public class KaptchaNotMatchException extends AuthenticationException {
public KaptchaNotMatchException(String msg, Throwable cause) {
super(msg, cause);
}
public KaptchaNotMatchException(String msg) {
super(msg);
}
}
- 修改 WebSecurityConfiguration 配置类,将自定义的过滤器进行替换
package com.vinjcent.config.security;
import com.vinjcent.filter.LoginKaptchaFilter;
import com.vinjcent.handler.DivAuthenticationFailureHandler;
import com.vinjcent.handler.DivAuthenticationSuccessHandler;
import com.vinjcent.handler.DivLogoutSuccessHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
private final DivUserDetailsService userDetailsService;
@Autowired
public WebSecurityConfiguration(DivUserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
@Override
@Bean
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Bean
public LoginKaptchaFilter loginKaptchaFilter() throws Exception {
LoginKaptchaFilter loginFilter = new LoginKaptchaFilter();
loginFilter.setFilterProcessesUrl("/login");
loginFilter.setUsernameParameter("uname");
loginFilter.setPasswordParameter("passwd");
loginFilter.setKaptchaParameter("kaptcha");
loginFilter.setAuthenticationManager(authenticationManager());
loginFilter.setAuthenticationSuccessHandler(new DivAuthenticationSuccessHandler());
loginFilter.setAuthenticationFailureHandler(new DivAuthenticationFailureHandler());
return loginFilter;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.mvcMatchers("/vc.jpg").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessHandler(new DivLogoutSuccessHandler())
.and()
.exceptionHandling()
.authenticationEntryPoint(((req, resp, ex) -> {
resp.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
resp.setStatus(HttpStatus.UNAUTHORIZED.value());
resp.getWriter().println("请认证之后再操作!");
}))
.and()
.csrf()
.disable();
http.addFilterAt(loginKaptchaFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
- 运行测试,使用postman测试工具进行测试
1)先进行请求/vc.jpg 生成图片,并将图片信息保存到 session 中
前端由Base64转为图片时,需要添加前缀:data:image/png;base64,

2)可以使用在线工具,解析Base64编码

3)解析之后再请求登录操作

|