一、文章简介

本文简要介绍了spring security的基本原理和实现,并基于springboot整合了spring security实现了基于数据库管理的用户的登录和登出,登录过程实现了验证码的校验功能。

完整代码地址:https://github.com/hello-shf/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', '', '', '8888@qq.com');
INSERT INTO `t_user` VALUES (2, '074aca14664b49ce9165bc597d928078', '2019-01-01 00:00:00', '2019-05-01 18:10:54', 0, '德华', 'admin', '', '', '8888@qq.com');
INSERT INTO `t_user` VALUES (3, '0bad7a4fea5f4c129c454cdf658744ec', '2019-01-01 00:00:00', '2019-05-01 18:11:13', 0, '富城', 'admin', '', '', '8888@qq.com');

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>

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;
}

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);
}

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);
}

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);
}
}

4.2.2 生成验证码的工具

验证码生产工具VerifyCodeUtil.java

 package com.shf.security.utils;

 import javax.servlet.http.HttpSession;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.*; /**
* 描述:
*
* @Author shf
* @Description TODO
* @Date 2019/5/5 10:53
* @Version V1.0
**/
public class VerifyCodeUtil {
public static final String SESSION_KEY = "verifyCode";
public static final String BUFFIMG_KEY = "buffImg";
/**
* 验证码图片的宽度。
*/
private static int width = 100;
public static final long VERIFYCODE_TIMEOUT = 30*1000;//一分钟 /**
* 验证码图片的高度。
*/
private static int height = 30; /**
* 验证码字符个数
*/
private static int codeCount = 4; /**
* 字体高度
*/
private static int fontHeight; /**
* 干扰线数量
*/
private static int interLine = 12; /**
* 第一个字符的x轴值,因为后面的字符坐标依次递增,所以它们的x轴值是codeX的倍数
*/
private static int codeX; /**
* codeY ,验证字符的y轴值,因为并行所以值一样
*/
private static int codeY; /**
* codeSequence 表示字符允许出现的序列值
*/
static 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' };
public static Map<String, Object> getVerifyCode(){
Map<String, Object> result = new HashMap<>();
//width-4 除去左右多余的位置,使验证码更加集中显示,减得越多越集中。
//codeCount+1 //等比分配显示的宽度,包括左右两边的空格
codeX = (width-4) / (codeCount+1);
//height - 10 集中显示验证码
fontHeight = height - 10;
codeY = height - 7;
// 定义图像buffer
BufferedImage buffImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics2D gd = buffImg.createGraphics();
// 创建一个随机数生成器类
Random random = new Random();
// 将图像填充为白色
gd.setColor(Color.WHITE);
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);
}
result.put(BUFFIMG_KEY, buffImg);
result.put(SESSION_KEY, randomCode.toString());
return result;
}
/**
* 定时删除session中存在的验证码信息
* @param session
*/
public static void removeAttrbute(final HttpSession session) {
final Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
session.removeAttribute(SESSION_KEY);
timer.cancel();
}
}, VERIFYCODE_TIMEOUT);
}
}

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.utils.VerifyCodeUtil;
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(VerifyCodeUtil.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行的配置,是我们实现ajax登录的关键。

 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 com.shf.security.utils.VerifyCodeUtil;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.awt.image.RenderedImage;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map; /**
* 描述:
*
* @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;
} @RequestMapping("/getVerifyCode")
public void getVerifyCode(HttpServletRequest request, HttpServletResponse response){
Map<String, Object> map = VerifyCodeUtil.getVerifyCode();
HttpSession session = request.getSession();
session.setAttribute(VerifyCodeUtil.SESSION_KEY, map.get(VerifyCodeUtil.SESSION_KEY));
// 禁止图像缓存。
response.setHeader("Pragma", "no-cache");
response.setHeader("Cache-Control", "no-cache");
response.setDateHeader("Expires", 0);
response.setContentType("image/jpeg");
// 将图像输出到Servlet输出流中。
try {
ServletOutputStream sos = response.getOutputStream();
ImageIO.write((RenderedImage) map.get(VerifyCodeUtil.BUFFIMG_KEY), "jpeg", sos);
sos.close();
//设置验证码过期时间
VerifyCodeUtil.removeAttrbute(session);
} catch (IOException e) {
e.printStackTrace();
}
}
}

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;
}
}

五、问题总结

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/hello-shf/spring-security.git开箱即用。

如有问题或者错误的地方,还请留言指出。

 

最新文章

  1. CNC系统实时性分析
  2. 分享25个新鲜出炉的 Photoshop 高级教程
  3. NOIP模拟赛-时间与空间之旅
  4. 移除virbr0
  5. Codevs 4600 [NOI2015]程序自动分析
  6. Ignatius and the Princess I
  7. iOS开发之OC篇-响应式编程Reactive Cocoa
  8. Sublime Text 关闭自动更新的办法
  9. 201521123019 《Java程序设计》第10周学习总结
  10. 网卡bond技术
  11. 新概念英语(1-133)Sensational news!
  12. 2019南昌邀请赛 L 计算几何 G(待补)
  13. PHP拦截器之__set()与__get()的理解与使用方法
  14. 关于n维和n-1维欧式空间
  15. javascript 利用冒泡机制显示与隐藏模态框
  16. R语言 实验三 数据探索和预处理
  17. WPF ContextMenu的使用
  18. 图片加载完毕后执行JS代码
  19. [python爬虫] 爬取图片无法打开或已损坏的简单探讨
  20. 微信 小程序布局 swiper 页面

热门文章

  1. DBCP2的使用例子和源码详解(不包括JNDI和JTA支持的使用)
  2. 一条数据的HBase之旅,简明HBase入门教程1:开篇
  3. “setTimeout、Promise、Async/Await 的区别”题目解析和扩展
  4. React-native ESLint &amp; Prettier &amp; Pre-commit Hook配置
  5. kubernetes学习笔记(三)——利用kubeadm部署集群
  6. [TimLinux] JavaScript 获取元素节点的5种方法
  7. 小白进阶—python中os模块用法
  8. 【JS】370- 总结异步编程的六种方式
  9. 【Canvas】311- 解决 canvas 在高清屏中绘制模糊的问题
  10. 探究UE4网络系列(二)、UE4网络核心类分析