六、自定义认证案例
6.1 传统web开发认证总结案例
-
创建一个spring-security-03模块
-
导入依赖
pom.xml
<dependencies>
<!--web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--security-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--thymeleaf-security-->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
<version>3.0.4.RELEASE</version>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.22</version>
</dependency>
<!--druid-->
<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
- User
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.*;
// 自定义用户User
public class User implements UserDetails {
private Integer id; // 用户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<>(); // 用户角色信息
// set和get方法以及重写的方法...(前面有提到)
}
- Role
package com.vinjcent.pojo;
import java.io.Serializable;
public class Role implements Serializable {
private Integer id;
private String name;
private String nameZh;
// set和get方法以及重写的方法...(前面有提到)
}
- 编写mapper、service、xml文件(这里只写接口,看接口实现方法)
- UserMapper
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;
/**
* @author vinjcent
* @description 针对表【user】的数据库操作Mapper
* @createDate 2022-09-25 12:03:42
*/
@Mapper
@Repository
public interface UserMapper {
// 根据用户名返回用户信息
User queryUserByUsername(@Param("username") String username);
}
- RoleMapper
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;
/**
* @author vinjcent
* @description 针对表【role】的数据库操作Mapper
* @createDate 2022-09-25 12:01:18
*/
@Mapper
@Repository
public interface RoleMapper {
List<Role> queryRolesByUid(@Param("uid") Integer uid);
}
- 自定义 DivUserDetailsService 实现 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 {
// dao ===> springboot + mybatis
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 {
// 1.查询用户
User user = userService.queryUserByUsername(username);
if (ObjectUtils.isEmpty(user)) throw new UsernameNotFoundException("用户名不正确!");
// 2.查询权限信息
List<Role> roles = roleService.queryRolesByUid(user.getId());
user.setRoles(roles);
return user;
}
}
- 配置类 WebMvcConfigurer、WebSecurityConfigurerAdapter
- WebMvcConfigurer
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) {
// 当访问url路径,映射一个view视图
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;
/**
* 重写 WebSecurityConfigurerAdapter 类使得默认 DefaultWebSecurityCondition 条件失效
*/
@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
// 构造注入使用@Autowired,set注入使用@Resource
private final DivUserDetailsService userDetailsService;
// UserDetailsService
@Autowired
public WebSecurityConfiguration(DivUserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
// AuthenticationManager
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
// 拦配置http拦截
@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") // 登出url
.logoutSuccessUrl("/toLogin") // 登出成功之后转发url
.and()
.csrf()
.disable();
}
}
- html页面视图
- index.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>
- login.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 数据格式进行交互,我们需要对认证用户信息的这个过滤器进行重写
- 对于 http 请求过滤器的配置
-
创建一个模块 spring-security-04-separate
-
导入依赖
pom.xml
<dependencies>
<!--web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--security-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.22</version>
</dependency>
<!--druid-->
<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:
# 注意 mapper 映射文件必须使用"/"
type-aliases-package: com.vinjcent.pojo
mapper-locations: com/vinjcent/mapper/**/*.xml
# 日志处理,为了展示 mybatis 运行 sql 语句
logging:
level:
com:
vinjcent:
debug
- 编写实体类User、Role
- User
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.*;
// 自定义用户User
public class User implements UserDetails {
private Integer id; // 用户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<>(); // 用户角色信息
// set和get方法以及重写的方法...(前面有提到)
}
- Role
package com.vinjcent.pojo;
import java.io.Serializable;
public class Role implements Serializable {
private Integer id;
private String name;
private String nameZh;
// set和get方法以及重写的方法...(前面有提到)
}
- 编写mapper、service、xml文件(这里只写接口,看接口实现方法)
- UserMapper
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;
/**
* @author vinjcent
* @description 针对表【user】的数据库操作Mapper
* @createDate 2022-09-25 12:03:42
*/
@Mapper
@Repository
public interface UserMapper {
// 根据用户名返回用户信息
User queryUserByUsername(@Param("username") String username);
}
- RoleMapper
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;
/**
* @author vinjcent
* @description 针对表【role】的数据库操作Mapper
* @createDate 2022-09-25 12:01:18
*/
@Mapper
@Repository
public interface RoleMapper {
List<Role> queryRolesByUid(@Param("uid") Integer uid);
}
- 自定义 UserDetailsService 实现 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 {
// dao ===> springboot + mybatis
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 {
// 1.查询用户
User user = userService.queryUserByUsername(username);
if (ObjectUtils.isEmpty(user)) throw new UsernameNotFoundException("用户名不正确!");
// 2.查询权限信息
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;
/**
* 自定义前后端分离的 Filter,重写 UsernamePasswordAuthenticationFilter
*/
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
// 用于指定请求类型
private boolean postOnly = true;
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
// 1.判断是否满足 POST 类型的请求
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
// 2.判断使用的数据格式类型是否是json
if (request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)) {
// 如果是json格式,需要转化成对象并从中获取用户输入的用户名和密码进行认证 {"username": "root","password": "123"}
try {
Map<String, String> userInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);
// 将用户名(username)和密码(password)通过动态传递的方式,进行获取
// getUsernameParameter()、getPasswordParameter()是父类的方法,通过父类设置这两个属性的值
String username = userInfo.get(getUsernameParameter());
String password = userInfo.get(getPasswordParameter());
System.out.println("用户名: " + username + " 密码: " + password);
// 生成用户令牌
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);
setDetails(request, token);
// 为了保证自定义的过滤器拥有 AuthenticationManager,我们还需手动配置一个
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
- WebSecurityConfiguration
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;
}
// 自定义AuthenticationManager(自定义需要暴露该bean)
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
// 暴露AuthenticationManager,使得这个bean能在组件中进行注入
@Override
@Bean
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Bean
public LoginFilter loginFilter() throws Exception {
// 1.创建自定义的LoginFilter对象
LoginFilter loginFilter = new LoginFilter();
// 2.设置登陆操作的请求
loginFilter.setFilterProcessesUrl("/login");
// 3.动态设置传递的参数key
loginFilter.setUsernameParameter("uname"); // 指定 json 中的用户名key
loginFilter.setPasswordParameter("passwd"); // 指定 json 中的密码key
// 4.设置自定义的用户认证管理者
loginFilter.setAuthenticationManager(authenticationManager());
// 5.配置认证成功/失败处理(前后端分离)
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") // 登出处理(也可以通过自定logoutRequestMatcher配置登出请求url和请求类型)
.logoutSuccessHandler(new DivLogoutSuccessHandler()) // 注销登录成功处理
.and()
.exceptionHandling() // 异常处理(用于未认证处理返回的数据)
.authenticationEntryPoint(((req, resp, ex) -> {
// 设置响应内容类型"application/json;charset=UTF-8"
resp.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
// 设置响应状态码为"未授权"401
resp.setStatus(HttpStatus.UNAUTHORIZED.value());
// 返回响应数据
resp.getWriter().println("请认证之后再操作!");
})) // 配置认证入口点异常处理
.and()
.csrf()
.disable(); // 关闭csrf跨域请求
// 替换原始 UsernamePasswordAuthenticationFilter 过滤器
http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
/**
http.addFilter(); // 添加一个过滤器
http.addFilterAt(); // at: 添加一个过滤器,将过滤链中的某个过滤器进行替换
http.addFilterBefore(); // before: 添加一个过滤器,追加到某个具体过滤器之前
http.addFilterAfter(); // after: 添加一个过滤器,追加到某个具体过滤器之后
*/
}
}
- 配置一个测试接口,作为请求资源进行测试
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
<!--verification-->
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>
- 添加验证码配置类 KaptchaConfiguration
- 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;
/**
* 自定义认证码,源自google
*/
@Configuration
public class KaptchaConfiguration {
@Bean
public Producer kaptcha() {
Properties properties = new Properties();
// 1.验证码宽度
properties.setProperty("kaptcha.image.width", "150");
// 2.验证码高度
properties.setProperty("kaptcha.image.height", "50");
// 3.验证码字符串
properties.setProperty("kaptcha.textproducer.char.string", "0123456789");
// 4.验证码长度
properties.setProperty("kaptcha.textproducer.char.length", "4");
Config config = new Config(properties);
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}
- 返回图片处理结果并存储在 session 中,编写 controller
- VerifyCodeController
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 {
// 1.生成验证码
String verifyCode = producer.createText();
// 2.保存到 session(可以存入到redis当中)
session.setAttribute("kaptcha", verifyCode);
// 3.生成图片
BufferedImage image = producer.createImage(verifyCode);
// 4.设置响应类型
response.setContentType("image/png#pic_center =800x");
// 5.响应图片
ServletOutputStream os = response.getOutputStream();
ImageIO.write(image, "jpg", os);
}
}
- 自定义登录验和证码过滤器
- LoginKaptchaFilter
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;
/**
* 自定义登录验证码 filter
*/
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 {
// 1.先判断是否为 POST 请求
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
// 2.从请求中获取验证码
String verifyCode = request.getParameter(getKaptchaParameter());
// 3.获取 session 中验证码进行比较
String sessionVerifyCode = (String) request.getSession().getAttribute("kaptcha");
// 4.与 session 中验证码进行比较
if (!ObjectUtils.isEmpty(verifyCode) && !ObjectUtils.isEmpty(sessionVerifyCode) && verifyCode.equals(sessionVerifyCode)) {
return super.attemptAuthentication(request, response);
}
// 5.如果匹配不上,抛出自定义异常
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;
}
}
- 自定义验证码不匹配异常
- KaptchaNotMatchException
package com.vinjcent.exception;
import org.springframework.security.core.AuthenticationException;
/**
* 自定义验证码异常 exception
*/
public class KaptchaNotMatchException extends AuthenticationException {
public KaptchaNotMatchException(String msg, Throwable cause) {
super(msg, cause);
}
public KaptchaNotMatchException(String msg) {
super(msg);
}
}
- 修改 WebSecurityConfiguration 配置类,将自定义的过滤器进行替换
- 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;
/**
* 重写 WebSecurityConfigurerAdapter 类使得默认 DefaultWebSecurityCondition 条件失效
*/
@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
// 构造注入使用@Autowired,set注入使用@Resource
private final DivUserDetailsService userDetailsService;
@Autowired
public WebSecurityConfiguration(DivUserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
// 自定义AuthenticationManager(自定义需要暴露该bean)
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
// 暴露AuthenticationManager,使得这个bean能在组件中进行注入
@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());
// 指定认证url
loginKaptchaFilter.setFilterProcessesUrl("/login");
// 指定认证成功处理
loginKaptchaFilter.setAuthenticationSuccessHandler(((request, response, authentication) -> {
response.sendRedirect("/index");
}));
// 指定认证失败处理
loginKaptchaFilter.setAuthenticationFailureHandler(((request, response, exception) -> {
response.sendRedirect("/toLogin");
}));
return loginKaptchaFilter;
}
// 拦配置http拦截
@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
<!--verification-->
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>
- 添加验证码配置类 KaptchaConfiguration
- 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;
/**
* 自定义认证码,源自google
*/
@Configuration
public class KaptchaConfiguration {
@Bean
public Producer kaptcha() {
Properties properties = new Properties();
// 1.验证码宽度
properties.setProperty("kaptcha.image.width", "150");
// 2.验证码高度
properties.setProperty("kaptcha.image.height", "50");
// 3.验证码字符串
properties.setProperty("kaptcha.textproducer.char.string", "0123456789");
// 4.验证码长度
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 {
// 1.生成验证码
String verifyCode = producer.createText();
// 2.保存到 session(可以存入到redis当中)
session.setAttribute("kaptcha", verifyCode);
// 3.生成图片
BufferedImage image = producer.createImage(verifyCode);
FastByteArrayOutputStream fos = new FastByteArrayOutputStream();
ImageIO.write(image, "jpg", fos);
// 4.将生成的图片转为Base64格式返回给前端
return Base64.encodeBase64String(fos.toByteArray());
}
}
- 自定义登录验和证码过滤器
- LoginKaptchaFilter
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;
/**
* 自定义登录验证码 filter
*/
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 {
// 1.先判断是否为 POST 请求
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
try {
// 2.通过key-value形式读取流中的文件
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());
// 3.获取 session 中的验证码
String sessionVerifyCode = (String) request.getSession().getAttribute("kaptcha");
// 4.将当前用户输入的验证码与 session 中的验证码进行比较
if (!ObjectUtils.isEmpty(verifyCode) && !ObjectUtils.isEmpty(sessionVerifyCode) && verifyCode.equals(sessionVerifyCode)) {
// 封装username&password的token
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
// 5.如果匹配不上,抛出自定义异常
throw new KaptchaNotMatchException("验证码不匹配!");
} catch (IOException e) {
e.printStackTrace();
}
// 2.从请求中获取验证码
String verifyCode = request.getParameter(getKaptchaParameter());
// 3.获取 session 中验证码进行比较
String sessionVerifyCode = (String) request.getSession().getAttribute("kaptcha");
// 4.与 session 中验证码进行比较
if (!ObjectUtils.isEmpty(verifyCode) && !ObjectUtils.isEmpty(sessionVerifyCode) && verifyCode.equals(sessionVerifyCode)) {
return super.attemptAuthentication(request, response);
}
// 5.如果匹配不上,抛出自定义异常
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;
}
}
- 自定义验证码不匹配异常
- KaptchaNotMatchException
package com.vinjcent.exception;
import org.springframework.security.core.AuthenticationException;
/**
* 自定义验证码异常 exception
*/
public class KaptchaNotMatchException extends AuthenticationException {
public KaptchaNotMatchException(String msg, Throwable cause) {
super(msg, cause);
}
public KaptchaNotMatchException(String msg) {
super(msg);
}
}
- 修改 WebSecurityConfiguration 配置类,将自定义的过滤器进行替换
- 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;
}
// 自定义AuthenticationManager(自定义需要暴露该bean)
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
// 暴露AuthenticationManager,使得这个bean能在组件中进行注入
@Override
@Bean
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Bean
public LoginKaptchaFilter loginKaptchaFilter() throws Exception {
// 1.创建自定义的LoginFilter对象
LoginKaptchaFilter loginFilter = new LoginKaptchaFilter();
// 2.设置登陆操作的请求
loginFilter.setFilterProcessesUrl("/login");
// 3.动态设置传递的参数key
loginFilter.setUsernameParameter("uname"); // 指定 json 中的用户名key
loginFilter.setPasswordParameter("passwd"); // 指定 json 中的密码key
loginFilter.setKaptchaParameter("kaptcha"); // 指定 json 中的验证码
// 4.设置自定义的用户认证管理者
loginFilter.setAuthenticationManager(authenticationManager());
// 5.配置认证成功/失败处理(前后端分离)
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") // 登出处理(也可以通过自定logoutRequestMatcher配置登出请求url和请求类型)
.logoutSuccessHandler(new DivLogoutSuccessHandler()) // 注销登录成功处理
.and()
.exceptionHandling() // 异常处理(用于未认证处理返回的数据)
.authenticationEntryPoint(((req, resp, ex) -> {
// 设置响应内容类型"application/json;charset=UTF-8"
resp.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
// 设置响应状态码为"未授权"401
resp.setStatus(HttpStatus.UNAUTHORIZED.value());
resp.getWriter().println("请认证之后再操作!");
})) // 配置认证入口点异常处理
.and()
.csrf()
.disable(); // 关闭csrf跨域请求
// 替换原始 UsernamePasswordAuthenticationFilter 过滤器
http.addFilterAt(loginKaptchaFilter(), UsernamePasswordAuthenticationFilter.class);
/**
http.addFilter(); // 添加一个过滤器
http.addFilterAt(); // at: 添加一个过滤器,将过滤链中的某个过滤器进行替换
http.addFilterBefore(); // before: 添加一个过滤器,追加到某个具体过滤器之前
http.addFilterAfter(); // after: 添加一个过滤器,追加到某个具体过滤器之后
*/
}
}
- 运行测试,使用postman测试工具进行测试
1)先进行请求/vc.jpg
生成图片,并将图片信息保存到 session 中
前端由Base64转为图片时,需要添加前缀:data:image/png;base64,
2)可以使用在线工具,解析Base64编码
3)解析之后再请求登录操作