一, 文章简介
本文简要介绍了 spring security 的基本原理和实现, 并基于 springboot 整合了 spring security 实现了基于数据库管理的用户的登录和登出, 登录过程实现了验证码的校验功能.
完整代码地址: https://github.com/Dreamshf/spring-security.git
二, spring security 框架简介
Spring Security 是一个能够为基于 Spring 的企业应用系统提供声明式的安全访问控制解决方案的安全框架. 主要包括: 用户认证 (Authentication) 和用户授权 (Authorization) 两个部分. 用户认证指的是验证某个用户能否访问该系统. 用户认证过程一般要求用户提供用户名和密码. 系统通过校验用户名和密码来完成认证过程. 用户授权指的是验证某个用户是否有权限执行某个操作或访问某个页面. 通常在一个企业级的系统中不同的用户所具有的权限也是不同的, 简单的来说比如普通用户和管理员的区别, 管理员显然具有更高的权限. 一般来说, 系统会为不同的用户分配不同的角色, 而每个角色则对应一系列的权限. spring security 的主要核心功能为认证和授权, 所有的架构也是基于这两个核心功能去实现的.
三, spring security 原理
Spring security 提供了一组可以在 Spring 应用上下文中配置的 Bean, 充分利用了 Spring IoC,DI, 和 AOP 功能, 为应用系统提供声明式的安全访问控制功能, 减少了为企业系统安全控制编写大量重复代码的工作. Spring Security 对 Web 安全性的支持大量地依赖于 Servlet 过滤器. 这些过滤器拦截进入请求, 并且在应用程序处理该请求之前进行某些安全处理. Spring Security 提供有若干个过滤器, 它们能够拦截 Servlet 请求, 并将这些请求转给认证和访问决策管理器处理, 从而增强安全性.
四, spring boot 整合 spring security
4.1 准备工作
4.1.1 数据库
- DROP TABLE IF EXISTS `t_user`;
- CREATE TABLE `t_user` (
- `id` int(10) NOT NULL AUTO_INCREMENT COMMENT '主键',
- `code` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '用户编码',
- `create_time` timestamp(0) NOT NULL DEFAULT '2019-01-01 00:00:00' COMMENT '注册时间',
- `update_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间',
- `is_delete` int(1) NOT NULL DEFAULT 0 COMMENT '是否删除 0: 未删除 1: 删除',
- `username` varchar(16) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户名',
- `password` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '密码',
- `role` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户角色',
- `phone` varchar(11) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '手机号',
- `email` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '邮箱',
- PRIMARY KEY (`id`) USING BTREE,
- UNIQUE INDEX `username`(`username`) USING BTREE
- ) ENGINE = InnoDB AUTO_INCREMENT = 14 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户表' ROW_FORMAT = Compact;
- INSERT INTO `t_user` VALUES (1, 'ef269d06e6b1497fbb209becca248251', '2019-04-22 14:24:10', '2019-04-29 06:55:39', 0, '学友', 'admin1', '1', '18888888888', '[email protected]');
- INSERT INTO `t_user` VALUES (2, '074aca14664b49ce9165bc597d928078', '2019-01-01 00:00:00', '2019-05-01 18:10:54', 0, '德华', 'admin', '1', '18839339393', '[email protected]');
- INSERT INTO `t_user` VALUES (3, '0bad7a4fea5f4c129c454cdf658744ec', '2019-01-01 00:00:00', '2019-05-01 18:11:13', 0, '富城', 'admin', '1', '18839339393', '[email protected]');
- View Code
4.1.2 pom.xml 依赖
- <?xml version="1.0" encoding="UTF-8"?>
- <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- <modelVersion>4.0.0</modelVersion>
- <parent>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-parent</artifactId>
- <version>1.5.10.RELEASE</version>
- <relativePath/> <!-- lookup parent from repository -->
- </parent>
- <groupId>com.shf</groupId>
- <artifactId>sping-boot-security</artifactId>
- <version>0.0.1-SNAPSHOT</version>
- <name>sping-boot-security</name>
- <description>Demo project for Spring Boot</description>
- <properties>
- <java.version>1.8</java.version>
- </properties>
- <dependencies>
- <dependency>
- <groupId>org.projectlombok</groupId>
- <artifactId>lombok</artifactId>
- </dependency>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-security</artifactId>
- <version>1.5.10.RELEASE</version>
- </dependency>
- <dependency>
- <groupId>MySQL</groupId>
- <artifactId>MySQL-connector-java</artifactId>
- </dependency>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-Web</artifactId>
- </dependency>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-data-jpa</artifactId>
- </dependency>
- </dependencies>
- <build>
- <plugins>
- <plugin>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-maven-plugin</artifactId>
- </plugin>
- </plugins>
- </build>
- </project>
- View Code
- 4.1.3 application.properties
- spring.datasource.url=jdbc:MySQL://localhost:3306/test?characterEncoding=utf8&useSSL=false&serverTimezone=GMT
- spring.datasource.driver-class-name=com.MySQL.jdbc.Driver
- spring.datasource.username=root
- spring.datasource.password=
4.2 代码实现
4.2.1 t_user 表的实体类 TUser 的基本操作
实体类的基本增删改查可依据项目需要自行选择合适的 ORM 框架, 此处我采用的是 jpa 实现的基本用户查询操作. 此模块不在过多赘述, 直接上代码
TUser.java 实体类
- package com.shf.security.user.entity;
- import lombok.Data;
- import javax.persistence.Entity;
- import javax.persistence.Id;
- import javax.persistence.Table;
- import java.util.Date;
- /**
- * 描述: 用户表实体
- * @author: shf
- * @date: 2019-04-19 16:24:04
- * @version: V1.0
- */
- @Data
- @Entity
- @Table(name = "t_user")
- public class TUser {
- /**
- * 主键
- */
- @Id
- private Integer id;
- /**
- * 用户编码
- */
- private String code;
- /**
- * 注册时间
- */
- private Date createTime;
- /**
- * 更新时间
- */
- private Date updateTime;
- /**
- * 是否删除 0: 删除 1: 未删除
- */
- private Integer isDelete;
- /**
- * 用户名
- */
- private String username;
- /**
- * 密码
- */
- private String password;
- /**
- * 用户角色
- */
- private String role;
- /**
- * 手机号
- */
- private String phone;
- /**
- * 邮箱
- */
- private String email;
- }
- View Code
TUserDao.java 类
- package com.shf.security.user.dao;
- import com.shf.security.user.entity.TUser;
- import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
- import org.springframework.data.jpa.repository.Query;
- import org.springframework.data.repository.CrudRepository;
- public interface TUserDao extends CrudRepository<TUser, Long>, JpaSpecificationExecutor<TUser> {
- @Query("select t from TUser t where t.username=?1")
- public TUser findByName(String username);
- }
- View Code
TUserService.java 接口
- package com.shf.security.user.service;
- import com.shf.security.user.entity.TUser;
- /**
- * 描述: 用户表服务类
- * @author: shf
- * @date: 2019-04-19 16:24:04
- * @version: V1.0
- */
- public interface TUserService{
- /**
- * @param username
- * @return
- */
- public TUser findByName(String username);
- }
- View Code
TUserServiceImpl.java
- package com.shf.security.user.service.impl;
- import com.shf.security.user.dao.TUserDao;
- import com.shf.security.user.entity.TUser;
- import com.shf.security.user.service.TUserService;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.stereotype.Service;
- /**
- * 描述:
- * @author: shf
- * @date: 2017/11/16 0016 13:12
- * @version: V1.0
- */
- @Service
- public class TUserServiceImpl implements TUserService {
- @Autowired
- private TUserDao userDao;
- @Override
- public TUser findByName(String username) {
- return userDao.findByName(username);
- }
- }
- View Code
4.2.2 生成验证码的工具
验证码生产工具 VerifyCodeFactory.java
- package com.shf.security.security.utils;
- import javax.imageio.ImageIO;
- import javax.servlet.ServletException;
- import javax.servlet.ServletOutputStream;
- import javax.servlet.http.HttpServlet;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
- import javax.servlet.http.HttpSession;
- import java.awt.*;
- import java.awt.image.BufferedImage;
- import java.io.IOException;
- import java.util.Random;
- /**
- * 描述: 验证码生成器
- *
- * @Author shf
- * @Date 2019/4/21 18:01
- * @Version V1.0
- **/
- public class VerifyCodeFactory extends HttpServlet {
- private static final long serialVersionUID = -5051097528828603895L;
- public final static String SESSION_KEY = "verifyCode";
- private final static int SESSION_TIME_OUT = 3600;
- /**
- * 验证码图片的宽度.
- */
- private int width = 100;
- /**
- * 验证码图片的高度.
- */
- private int height = 30;
- /**
- * 验证码字符个数
- */
- private int codeCount = 4;
- /**
- * 字体高度
- */
- private int fontHeight;
- /**
- * 干扰线数量
- */
- private int interLine = 12;
- /**
- * 第一个字符的 x 轴值, 因为后面的字符坐标依次递增, 所以它们的 x 轴值是 codeX 的倍数
- */
- private int codeX;
- /**
- * codeY , 验证字符的 y 轴值, 因为并行所以值一样
- */
- private int codeY;
- /**
- * codeSequence 表示字符允许出现的序列值
- */
- char[] codeSequence = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
- 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W',
- 'X', 'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
- /**
- * 初始化验证图片属性
- */
- @Override
- public void init() throws ServletException {
- // 从 Web.xml 中获取初始信息
- // 宽度
- String strWidth = this.getInitParameter("width");
- // 高度
- String strHeight = this.getInitParameter("height");
- // 字符个数
- String strCodeCount = this.getInitParameter("codeCount");
- // 将配置的信息转换成数值
- try {
- if (strWidth != null && strWidth.length() != 0) {
- width = Integer.parseInt(strWidth);
- }
- if (strHeight != null && strHeight.length() != 0) {
- height = Integer.parseInt(strHeight);
- }
- if (strCodeCount != null && strCodeCount.length() != 0) {
- codeCount = Integer.parseInt(strCodeCount);
- }
- } catch (NumberFormatException e) {
- e.printStackTrace();
- }
- //width-4 除去左右多余的位置, 使验证码更加集中显示, 减得越多越集中.
- //codeCount+1 // 等比分配显示的宽度, 包括左右两边的空格
- codeX = (width-4) / (codeCount+1);
- //height - 10 集中显示验证码
- fontHeight = height - 10;
- codeY = height - 7;
- }
- /**
- * @param request
- * @param response
- * @throws ServletException
- * @throws IOException
- */
- @Override
- protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
- // 定义图像 buffer
- BufferedImage buffImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
- Graphics2D gd = buffImg.createGraphics();
- // 创建一个随机数生成器类
- Random random = new Random();
- // 将图像填充为白色
- gd.setColor(Color.LIGHT_GRAY);
- gd.fillRect(0, 0, width, height);
- // 创建字体, 字体的大小应该根据图片的高度来定.
- Font font = new Font("Times New Roman", Font.PLAIN, fontHeight);
- // 设置字体.
- gd.setFont(font);
- // 画边框.
- gd.setColor(Color.BLACK);
- gd.drawRect(0, 0, width - 1, height - 1);
- // 随机产生 16 条干扰线, 使图象中的认证码不易被其它程序探测到.
- gd.setColor(Color.gray);
- for (int i = 0; i <interLine; i++) {
- int x = random.nextInt(width);
- int y = random.nextInt(height);
- int xl = random.nextInt(12);
- int yl = random.nextInt(12);
- gd.drawLine(x, y, x + xl, y + yl);
- }
- // randomCode 用于保存随机产生的验证码, 以便用户登录后进行验证.
- StringBuffer randomCode = new StringBuffer();
- int red = 0, green = 0, blue = 0;
- // 随机产生 codeCount 数字的验证码.
- for (int i = 0; i < codeCount; i++) {
- // 得到随机产生的验证码数字.
- String strRand = String.valueOf(codeSequence[random.nextInt(36)]);
- // 产生随机的颜色分量来构造颜色值, 这样输出的每位数字的颜色值都将不同.
- red = random.nextInt(255);
- green = random.nextInt(255);
- blue = random.nextInt(255);
- // 用随机产生的颜色将验证码绘制到图像中.
- gd.setColor(new Color(red,green,blue));
- gd.drawString(strRand, (i + 1) * codeX, codeY);
- // 将产生的四个随机数组合在一起.
- randomCode.append(strRand);
- }
- // 将四位数字的验证码保存到 Session 中.
- HttpSession session = request.getSession();
- session.setAttribute(SESSION_KEY, randomCode.toString());
- session.setMaxInactiveInterval(SESSION_TIME_OUT);
- // 禁止图像缓存.
- response.setHeader("Pragma", "no-cache");
- response.setHeader("Cache-Control", "no-cache");
- response.setDateHeader("Expires", 0);
- response.setContentType("image/jpeg");
- // 将图像输出到 Servlet 输出流中.
- ServletOutputStream sos = response.getOutputStream();
- ImageIO.write(buffImg, "jpeg", sos);
- sos.close();
- }
- }
- View Code
将验证码以接口的方式对外暴露
- package com.shf.security.security.config;
- import com.shf.security.security.utils.VerifyCodeFactory;
- import org.springframework.boot.Web.servlet.ServletRegistrationBean;
- import org.springframework.context.annotation.Bean;
- import org.springframework.context.annotation.Configuration;
- /**
- * 描述: 验证码 servlet 配置项
- *
- * @Author shf
- * @Date 2019/4/21 18:05
- * @Version V1.0
- **/
- @Configuration
- public class VerifyCodeServletConfig {
- @Bean
- public ServletRegistrationBean indexServletRegistration() {
- ServletRegistrationBean registration = new ServletRegistrationBean(new VerifyCodeFactory());
- registration.addUrlMappings("/getVerifyCode");
- return registration;
- }
- }
- View Code
4.2.3 自定义用户信息类 CustomUserDetails 集成实体类 TUser 并实现 security 提供的 UserDetails 接口
UserDetails 是真正用于构建 SpringSecurity 登录的安全用户(UserDetails), 也就是说, 在 springsecurity 进行用户认证的过程中, 是通过 UserDetails 的实现类去获取用户信息, 然后进行授权验证的. 不明白? 没关系, 继续往下看
- package com.shf.security.security.config;
- import com.shf.security.user.entity.TUser;
- import org.springframework.security.core.GrantedAuthority;
- import org.springframework.security.core.userdetails.UserDetails;
- import java.util.Collection;
- /**
- * 描述: 自定义 UserDetails, 使 UserDetails 具有 TUser 的实体结构
- *
- * @Author shf
- * @Date 2019/4/19 10:30
- * @Version V1.0
- **/
- public class CustomUserDetails extends TUser implements UserDetails {
- public CustomUserDetails(TUser tUser){
- if(null != tUser){
- this.setId(tUser.getId());
- this.setCode(tUser.getCode());
- this.setCreateTime(tUser.getCreateTime());
- this.setUpdateTime(tUser.getUpdateTime());
- this.setUsername(tUser.getUsername());
- this.setPassword(tUser.getPassword());
- this.setIsDelete(tUser.getIsDelete());
- this.setEmail(tUser.getEmail());
- this.setPhone(tUser.getPhone());
- this.setRole(tUser.getRole());
- }
- }
- @Override
- public Collection<? extends GrantedAuthority> getAuthorities() {
- return null;
- }
- @Override
- public boolean isAccountNonExpired() {
- return true;
- }
- @Override
- public boolean isAccountNonLocked() {
- return true;
- }
- @Override
- public boolean isCredentialsNonExpired() {
- return true;
- }
- @Override
- public boolean isEnabled() {
- return true;
- }
- }
4.2.4 创建 CustomUserDetailsService 类实现 UserDetailsService 接口
在下文将要提到的 CustomAuthenticationProvider 类, 也就是 security 核心的验证类中, 会调用 CustomUserDetailsService 中重写的 loadUserByUsername 方法
- package com.shf.security.security.config;
- import com.shf.security.user.entity.TUser;
- import com.shf.security.user.service.TUserService;
- 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;
- /**
- * 描述: 自定义 UserDetailsService, 从数据库读取用户信息, 实现登录验证
- *
- * @Author shf
- * @Date 2019/4/21 17:21
- * @Version V1.0
- **/
- @Component
- public class CustomUserDetailsService implements UserDetailsService {
- @Autowired
- private TUserService userService;
- /**
- * 认证过程中 - 根据登录信息获取用户详细信息
- *
- * @param username 登录用户输入的用户名
- * @return
- * @throws UsernameNotFoundException
- */
- @Override
- public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
- // 根据用户输入的用户信息, 查询数据库中已注册用户信息
- TUser user = userService.findByName(username);
- // 如果用户不存在直接抛出 UsernameNotFoundException 异常
- if (user == null) throw new UsernameNotFoundException("用户名为" + username + "的用户不存在");
- return new CustomUserDetails(user);
- }
- }
4.2.5 新建类 CustomWebAuthenticationDetails 继承 WebAuthenticationDetails 类
类似于 UserDetails 类给我们提供了用户详细信息一样, WebAuthenticationDetails 则为我们提供了登录请求的用户的信息(也就是申请登录的用户的 username 和 password 信息),springsecurity 默认只验证用户的 username 和 password 信息, 所以我们如果想实现验证码登录, 需要重写 WebAuthenticationDetails 类, 使其能通过 HttpServletRequest 获取到用户输入的验证码的信息.
- package com.shf.security.security.config;
- import org.springframework.security.Web.authentication.WebAuthenticationDetails;
- import javax.servlet.http.HttpServletRequest;
- /**
- * 描述: 自定义 WebAuthenticationDetails, 将验证码和用户名, 密码一同带入 AuthenticationProvider 中
- *
- * @Author shf
- * @Date 2019/4/21 16:58
- * @Version V1.0
- **/
- public class CustomWebAuthenticationDetails extends WebAuthenticationDetails {
- private static final long serialVersionUID = 6975601077710753878L;
- private final String verifyCode;
- public CustomWebAuthenticationDetails(HttpServletRequest request) {
- super(request);
- verifyCode = request.getParameter("verifyCode");
- }
- public String getVerifyCode() {
- return verifyCode;
- }
- @Override
- public String toString() {
- StringBuilder sb = new StringBuilder();
- sb.append(super.toString()).append("; verifyCode:").append(this.getVerifyCode());
- return sb.toString();
- }
- }
4.2.6 创建 CustomAuthenticationDetailsSource 类继承 AuthenticationDetailsSource 类
上面提到 CustomWebAuthenticationDetails 需要通过 HttpServletRequest 获取到用户输入的验证码的信息. AuthenticationDetailsSource 类就是初始化 CustomWebAuthenticationDetails 类的地方, 在这里面我们需要将 HttpServletRequest 传递到 CustomAuthenticationDetailsSource 中.
- package com.shf.security.security.config;
- import org.springframework.security.authentication.AuthenticationDetailsSource;
- import org.springframework.security.Web.authentication.WebAuthenticationDetails;
- import org.springframework.stereotype.Component;
- import javax.servlet.http.HttpServletRequest;
- /**
- * 描述: 自定义 AuthenticationDetailsSource, 将 HttpServletRequest 注入到 CustomWebAuthenticationDetails, 使其能获取到请求中的验证码等其他信息
- *
- * @Author shf
- * @Date 2019/4/21 17:03
- * @Version V1.0
- **/
- @Component
- public class CustomAuthenticationDetailsSource implements AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> {
- @Override
- public WebAuthenticationDetails buildDetails(HttpServletRequest request) {
- return new CustomWebAuthenticationDetails(request);
- }
- }
4.2.7 实现自定义认证器(重点), 创建 CustomAuthenticationProvider 继承 AbstractUserDetailsAuthenticationProvider 类
AbstractUserDetailsAuthenticationProvider 类实现的是 AuthenticationProvider 接口
- package com.shf.security.security.config;
- import com.shf.security.security.utils.VerifyCodeFactory;
- import lombok.extern.slf4j.Slf4j;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.security.authentication.BadCredentialsException;
- import org.springframework.security.authentication.DisabledException;
- import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
- import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider;
- import org.springframework.security.core.Authentication;
- import org.springframework.security.core.AuthenticationException;
- import org.springframework.security.core.userdetails.UserDetails;
- import org.springframework.stereotype.Component;
- import org.springframework.Web.context.request.RequestContextHolder;
- import org.springframework.Web.context.request.ServletRequestAttributes;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpSession;
- /**
- * 描述: 自定义 SpringSecurity 的认证器
- *
- * @Author shf
- * @Date 2019/4/21 17:30
- * @Version V1.0
- **/
- @Component
- @Slf4j
- public class CustomAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {//implements AuthenticationProvider {
- @Autowired
- private CustomUserDetailsService userDetailsService;
- @Override
- protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken) throws AuthenticationException {
- }
- @Override
- public Authentication authenticate(Authentication authentication) throws AuthenticationException {
- // 用户输入的用户名
- String username = authentication.getName();
- // 用户输入的密码
- String password = authentication.getCredentials().toString();
- // 通过 CustomWebAuthenticationDetails 获取用户输入的验证码信息
- CustomWebAuthenticationDetails details = (CustomWebAuthenticationDetails) authentication.getDetails();
- String verifyCode = details.getVerifyCode();
- if(null == verifyCode || verifyCode.isEmpty()){
- log.warn("未输入验证码");
- throw new NullPointerException("请输入验证码");
- }
- // 校验验证码
- if(!validateVerifyCode(verifyCode)){
- log.warn("验证码输入错误");
- throw new DisabledException("验证码输入错误");
- }
- // 通过自定义的 CustomUserDetailsService, 以用户输入的用户名查询用户信息
- CustomUserDetails userDetails = (CustomUserDetails) userDetailsService.loadUserByUsername(username);
- // 校验用户密码
- if(!userDetails.getPassword().equals(password)){
- log.warn("密码错误");
- throw new BadCredentialsException("密码错误");
- }
- Object principalToReturn = userDetails;
- // 将用户信息塞到 SecurityContext 中, 方便获取当前用户信息
- return this.createSuccessAuthentication(principalToReturn, authentication, userDetails);
- }
- @Override
- protected UserDetails retrieveUser(String s, UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken) throws AuthenticationException {
- return null;
- }
- /**
- * 验证用户输入的验证码
- * @param inputVerifyCode
- * @return
- */
- public boolean validateVerifyCode(String inputVerifyCode){
- // 获取当前线程绑定的 request 对象
- HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
- // 这个 VerifyCodeFactory.SESSION_KEY 是在 servlet 中存入 session 的名字
- HttpSession session = request.getSession();
- String verifyCode = (String)session.getAttribute(VerifyCodeFactory.SESSION_KEY);
- if(null == verifyCode || verifyCode.isEmpty()){
- log.warn("验证码过期请重新验证");
- throw new DisabledException("验证码过期, 请重新验证");
- }
- // 不分区大小写
- verifyCode = verifyCode.toLowerCase();
- inputVerifyCode = inputVerifyCode.toLowerCase();
- log.info("验证码:{}, 用户输入:{}", verifyCode, inputVerifyCode);
- return verifyCode.equals(inputVerifyCode);
- }
- @Override
- public boolean supports(Class<?> authentication) {
- return authentication.equals(UsernamePasswordAuthenticationToken.class);
- }
- }
如上图所示, AuthenticationProvider 接口为我们提供了 security 核心的认证方法 authenticate 方法, 该方法就是实现用户认证的方法. 我们自定义实现 authenticate 方法, 大致思路如下, 通过 CustomWebAuthenticationDetails 获取到用户输入的 username,password,verifyCode 信息. 通过 CustomUserDetails 中获取用户信息(数据库中注册的用户的信息), 然后对用户信息进行比对认证. 最终实现认证过程.
当然, 也可以直接实现 AuthenticationProvider 接口, 然后实现 authenticate 方法. 这都是可以的但是有现成的 AbstractUserDetailsAuthenticationProvider 可用, 为啥还要再写一遍呢? 尤其是 AbstractUserDetailsAuthenticationProvider 类提供的 createSuccessAuthentication 方法, 封装了一个完美的 Authentication(后续会继续提到).AuthenticationProvider 的 supports 方法呢是直接决定哪一个 AuthenticationProvider 的实现类是我们需要的认证器.
4.2.8 创建 WebSecurityConfig 继承 WebSecurityConfigurerAdapter 配置类.(spring security 的配置类)
具体看代码注释吧, 很详细的.
值得一提的是第 81 行的配置, 是我们实现 restful 风格登录的关键.
- package com.shf.security.security.config;
- import lombok.extern.slf4j.Slf4j;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.context.annotation.Bean;
- import org.springframework.context.annotation.Configuration;
- import org.springframework.security.authentication.AuthenticationDetailsSource;
- 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.builders.WebSecurity;
- import org.springframework.security.config.annotation.Web.configuration.EnableWebSecurity;
- import org.springframework.security.config.annotation.Web.configuration.WebSecurityConfigurerAdapter;
- import org.springframework.security.core.Authentication;
- import org.springframework.security.core.AuthenticationException;
- import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
- import org.springframework.security.crypto.password.PasswordEncoder;
- import org.springframework.security.Web.authentication.AuthenticationFailureHandler;
- import org.springframework.security.Web.authentication.AuthenticationSuccessHandler;
- import org.springframework.security.Web.authentication.WebAuthenticationDetails;
- 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.io.PrintWriter;
- /**
- * 描述:
- *
- * @Author shf
- * @Date 2019/4/19 10:54
- * @Version V1.0
- **/
- @Configuration
- @EnableWebSecurity
- @Slf4j
- public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
- @Autowired
- private CustomAuthenticationProvider customAuthenticationProvider;
- @Autowired
- private CustomUserDetailsService customUserDetailsService;
- @Autowired
- private AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> authenticationDetailsSource;
- @Override
- protected void configure(AuthenticationManagerBuilder auth) throws Exception {
- // 将自定义的 CustomAuthenticationProvider 装配到 AuthenticationManagerBuilder
- auth.authenticationProvider(customAuthenticationProvider);
- // 将自定的 CustomUserDetailsService 装配到 AuthenticationManagerBuilder
- auth.userDetailsService(customUserDetailsService).passwordEncoder(new PasswordEncoder() {
- @Override
- public String encode(CharSequence charSequence) {
- return charSequence.toString();
- }
- @Override
- public boolean matches(CharSequence charSequence, String s) {
- return s.equals(charSequence.toString());
- }
- });
- }
- @Override
- public void configure(HttpSecurity http) throws Exception {
- http
- .cors()
- .and().csrf().disable();// 开启跨域
- http /* 匿名请求: 不需要进行登录拦截的 url*/
- .authorizeRequests()
- .antMatchers("/getVerifyCode").permitAll()
- .anyRequest().authenticated()// 其他的路径都是登录后才可访问
- .and()
- /* 登录配置 */
- .formLogin()
- .loginPage("/login_page")// 登录页, 当未登录时会重定向到该页面
- .successHandler(authenticationSuccessHandler())// 登录成功处理
- .failureHandler(authenticationFailureHandler())// 登录失败处理
- .authenticationDetailsSource(authenticationDetailsSource)// 自定义验证逻辑, 增加验证码信息
- .loginProcessingUrl("/login")//restful 登录请求地址
- .usernameParameter("username")// 默认的用户名参数
- .passwordParameter("password")// 默认的密码参数
- .permitAll()
- .and()
- /* 登出配置 */
- .logout()
- .permitAll()
- .logoutSuccessHandler(logoutSuccessHandler());
- }
- /**
- * security 检验忽略的请求, 比如静态资源不需要登录的可在本处配置
- * @param Web
- */
- @Override
- public void configure(WebSecurity Web){
- // platform.ignoring().antMatchers("/");
- }
- @Autowired
- public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
- auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());
- auth.eraseCredentials(false);
- }
- // 密码加密配置
- @Bean
- public BCryptPasswordEncoder passwordEncoder() {
- return new BCryptPasswordEncoder(4);
- }
- // 登入成功
- @Bean
- public AuthenticationSuccessHandler authenticationSuccessHandler() {
- return new AuthenticationSuccessHandler() {
- /**
- * 处理登入成功的请求
- *
- * @param httpServletRequest
- * @param httpServletResponse
- * @param authentication
- * @throws IOException
- * @throws ServletException
- */
- @Override
- public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
- httpServletResponse.setContentType("application/json;charset=utf-8");
- PrintWriter out = httpServletResponse.getWriter();
- out.write("{\"status\":\"success\",\"msg\":\" 登录成功 \"}");
- out.flush();
- out.close();
- }
- };
- }
- // 登录失败
- @Bean
- public AuthenticationFailureHandler authenticationFailureHandler(){
- return new AuthenticationFailureHandler() {
- /**
- * 处理登录失败的请求
- * @param httpServletRequest
- * @param httpServletResponse
- * @param e
- * @throws IOException
- * @throws ServletException
- */
- @Override
- public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
- httpServletResponse.setContentType("application/json;charset=utf-8");
- PrintWriter out = httpServletResponse.getWriter();
- out.write("{\"status\":\"error\",\"msg\":\" 登录失败 \"}");
- out.flush();
- out.close();
- }
- };
- }
- // 登出处理
- @Bean
- public LogoutSuccessHandler logoutSuccessHandler() {
- return new LogoutSuccessHandler() {
- /**
- * 处理登出成功的请求
- *
- * @param httpServletRequest
- * @param httpServletResponse
- * @param authentication
- * @throws IOException
- * @throws ServletException
- */
- @Override
- public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
- httpServletResponse.setContentType("application/json;charset=utf-8");
- PrintWriter out = httpServletResponse.getWriter();
- out.write("{\"status\":\"success\",\"msg\":\" 登出成功 \"}");
- out.flush();
- out.close();
- }
- };
- }
- }
- 4.2.9 LoginController
- package com.shf.security.login;
- import com.shf.security.utils.Response;
- import org.springframework.Web.bind.annotation.RequestMapping;
- import org.springframework.Web.bind.annotation.RestController;
- /**
- * 描述:
- *
- * @Author shf
- * @Date 2019/4/19 14:58
- * @Version V1.0
- **/
- @RestController
- public class LoginController {
- @RequestMapping("/login_error")
- public Response loginError(){
- Response response = new Response();
- response.buildSuccessResponse("登录失败");
- return response;
- }
- @RequestMapping("/login_success")
- public Response loginSuccess(){
- Response response = new Response();
- response.buildSuccessResponse("登录成功");
- return response;
- }
- @RequestMapping("/login_page")
- public Response root(){
- Response response = new Response();
- response.buildSuccessResponse("尚未登录, 请登录");
- return response;
- }
- }
- View Code
4.2.10 UserHolder 工具类
在日常的业务中, 在很多业务代码中, 我们都需要获取当前用户的信息. 这个类就是一个静态工具类.
- package com.shf.security.utils;
- import com.shf.security.user.entity.TUser;
- import org.springframework.security.core.Authentication;
- import org.springframework.security.core.context.SecurityContext;
- import org.springframework.security.core.context.SecurityContextHolder;
- /**
- * 描述:
- *
- * @Author shf
- * @Description TODO
- * @Date 2019/4/21 15:24
- * @Version V1.0
- **/
- public class UserHolder {
- public static TUser getUserDetail(){
- SecurityContext ctx = SecurityContextHolder.getContext();
- Authentication auth = ctx.getAuthentication();
- TUser user = (TUser) auth.getPrincipal();
- return user;
- }
- public static String getUserCode(){
- SecurityContext ctx = SecurityContextHolder.getContext();
- Authentication auth = ctx.getAuthentication();
- TUser user = (TUser) auth.getPrincipal();
- return user.getCode();
- }
- public static int getUserId(){
- SecurityContext ctx = SecurityContextHolder.getContext();
- Authentication auth = ctx.getAuthentication();
- TUser user = (TUser) auth.getPrincipal();
- return user.getId();
- }
- }
4.2.10 其他工具类 Response.java
- package com.shf.security.utils;
- import lombok.Data;
- /**
- * 描述:
- *
- * @Author shf
- * @Description TODO
- * @Date 2019/4/16 15:03
- * @Version V1.0
- **/
- @Data
- public class Response {
- private String code;
- private String msg;
- private Object data;
- public Response() {
- this.code = "-200";
- this.msg = "SUCCESS";
- }
- public Response(String code, String msg){
- this.code = code;
- this.msg = msg;
- }
- public Response buildSuccessResponse(){
- this.code = "-200";
- this.msg = "SUCCESS";
- return this;
- }
- public Response buildFailedResponse(){
- this.code = "-400";
- this.msg = "FAILED";
- return this;
- }
- public Response buildSuccessResponse(String msg){
- this.code = "-200";
- this.msg = msg;
- return this;
- }
- public Response buildFailedResponse(String msg){
- this.code = "-400";
- this.msg = msg;
- return this;
- }
- public Response buildFailedResponse(String code, String msg){
- this.code = code;
- this.msg = msg;
- return this;
- }
- public Response buildSuccessResponse(String code, String msg){
- this.code = code;
- this.msg = msg;
- return this;
- }
- }
- View Code
五, 问题总结
5.1 验证码问题
其实呢通过第二部分对 security 原理的分析, 我们不难看出, spring security 就是建立在一连串的过滤器 filter 上的, spring security 通过这些过滤器逐层对请求进行过滤, 然后进行各种登录认证和授权过程. 说道这里估计大家也就能想到另外的实现验证码验证登录的方式. 也就是在认证用户输入的用户名和密码之前验证验证码信息. UsernamePasswordAuthenticationFilter 过滤器顾名思义就是用户名和密码的过滤器. 所以我们只需要在 4.2.8 章节中的 WebSecurityConfig 中 addFilterBefore()配置在 UsernamePasswordAuthenticationFilter 过滤器之前执行 VerifyCodeFilter 过滤器. 然后在 VerifyCodeFilter 过滤器中执行验证码的验证逻辑即可.
- .and()
- .addFilterBefore(new VerifyCodeFilter(),UsernamePasswordAuthenticationFilter.class)
但是这种方式呢有一种天然的缺点, 也就是没法办将除 username 和 password 的信息带到认证器中进行统一认证. 而且如果我们除了验证码意外还需要验证更多的信息的话. 岂不是要写 n 多个 filter.
5.2 貌似忘了进行测试登录
浏览器请求: http://localhost:8080/user/test
结果:
正是我们想要的结果.
登录验证还是使用 postman 吧, 因为 spring security 默认只处理 post 方式的登录请求. 浏览器提交 restful 请求默认是 get 的. 所以...
postman 请求验证码
postman 登录
看到这里如果还有问题, 请移步 https://github.com/Dreamshf/spring-security.git 开箱即用.
如有问题或者错误的地方, 还请留言指出.
来源: http://www.bubuko.com/infodetail-3043495.html