在开始本文之前,底层这块已经有了很大的调整,主要是SpringBoot由之前的 1.5.9.RELEASE 升级至 2.1.0.RELEASE 版本,其它依赖的三方包基本也都升级到目前最新版了。
其次是整体架构上也做了调整:
sunny-parent:sunny 项目的顶级父类,sunny-parent 又继承自 spring-boot-starter-parent ,为所有项目统一 spring 及 springboot 版本。同时,管理项目中将用到的大部分的第三方包,统一管理版本号。
sunny-starter:项目中开发的组件以 starter 的方式进行集成,按需引入 starter 即可。sunny-starter 下以 module 的形式组织,便于管理、批量打包部署。
sunny-starter-core:核心包,定义基础的操作类、异常封装、工具类等,集成了 mybatis-mapper、druid 数据源、redis 等。
sunny-starter-captcha:验证码封装。
sunny-cloud:spring-cloud 系列服务,微服务基础框架,本篇文章主要集中在 sunny-cloud-security上,其它的以后再说。
sunny-cloud-security:认证服务和授权服务。
sunny-admin:管理端服务,业务中心。
SpringSecurity 是专门针对基于Spring项目的安全框架,充分利用了AOP和Filter来实现安全功能。它提供全面的安全性解决方案,同时在 Web 请求级和方法调用级处理身份确认和授权。他提供了强大的企业安全服务,如:认证授权机制、Web资源访问控制、业务方法调用访问控制、领域对象访问控制Access Control List(ACL)、单点登录(SSO)等等。
核心功能:认证(你是谁)、授权(你能干什么)、攻击防护(防止伪造身份)。
基本原理:SpringSecurity的核心实质是一个过滤器链,即一组Filter,所有的请求都会经过这些过滤器,然后响应返回。每个过滤器都有特定的职责,可通过配置添加、删除过滤器。过滤器的排序很重要,因为它们之间有依赖关系。有些过滤器也不能删除,如处在过滤器链最后几环的ExceptionTranslationFilter(处理后者抛出的异常),FilterSecurityInterceptor(最后一环,根据配置决定请求能不能访问服务)。
使用 用户名+密码 的方式来登录,用户名、密码存储在数据库,并且支持密码输入错误三次后开启验证码,通过这样一个过程来熟悉 spring security 的认证流程,掌握 spring security 的原理。
1、基础环境
① 创建 sunny-cloud-security 模块,端口号设置为 8010,在sunny-cloud-security模块引入security支持以及sunny-starter-core:
③ 不做任何配置,启动系统,然后访问 localhost:8010/test 时,会自动跳转到SpringSecurity默认的登录页面去进行认证。那这登录的用户名和密码从哪来呢?
启动项目时,从控制台输出中可以找到生成的 security 密码,从 UserDetailsServiceAutoConfiguration 可以得知,使用的是基于内存的用户管理器,默认的用户名为 user,密码是随机生成的UUID。
④ 使用 user 和生成的UUID密码登录成功后即可访问 /test 资源,最简单的一个认证就完成了。
2、自定义登录页面
① 首先开发一个登录页面,由于页面中会使用到一些动态数据,决定使用 thymeleaf 模板引擎,只需在 pom 中引入如下依赖,使用默认配置即可,具体有哪些配置可从 ThymeleafProperties 中了解到。
② 同时,在 resources 目录下,建 static 和 templates 两个目录,static 目录用于存放静态资源,templates 用于存放 thymeleaf 模板页面,同时配置MVC的静态资源映射。
③ 开发后台首页、登录页面的跳转地址,/login 接口用于向登录页面传递登录相关的数据,如用户名、是否启用验证码、错误消息等。
package com.lyyzoo.sunny.security.controller;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.web.WebAttributes;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import com.lyyzoo.sunny.captcha.CaptchaImageHelper;
import com.lyyzoo.sunny.core.base.Result;
import com.lyyzoo.sunny.core.message.MessageAccessor;
import com.lyyzoo.sunny.core.userdetails.CustomUserDetails;
import com.lyyzoo.sunny.core.userdetails.DetailsHelper;
import com.lyyzoo.sunny.core.util.Results;
import com.lyyzoo.sunny.security.constant.SecurityConstants;
import com.lyyzoo.sunny.security.domain.entity.User;
import com.lyyzoo.sunny.security.domain.service.ConfigService;
import com.lyyzoo.sunny.security.domain.service.UserService;
/**
*
* @author bojiangzhou 2018/03/28
*/
@Controller
public class SecurityController {
private static final String LOGIN_PAGE = "login";
private static final String INDEX_PAGE = "index";
private static final String FIELD_ERROR_MSG = "errorMsg";
private static final String FIELD_ENABLE_CAPTCHA = "enableCaptcha";
@Autowired
private CaptchaImageHelper captchaImageHelper;
@Autowired
private UserService userService;
@Autowired
private ConfigService configService;
@RequestMapping("/index")
public String index() {
return INDEX_PAGE;
}
@GetMapping("/login")
public String login(HttpSession session, Model model) {
String errorMsg = (String) session.getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
String username = (String) session.getAttribute(User.FIELD_USERNAME);
if (StringUtils.isNotBlank(errorMsg)) {
model.addAttribute(FIELD_ERROR_MSG, errorMsg);
}
if (StringUtils.isNotBlank(username)) {
model.addAttribute(User.FIELD_USERNAME, username);
User user = userService.getUserByUsername(username);
if (user == null) {
model.addAttribute(FIELD_ERROR_MSG, MessageAccessor.getMessage("login.username-or-password.error"));
} else {
//用户名密码正确后这里进行验证码的校验
if (configService.isEnableCaptcha(user.getPasswordErrorTime())) {
model.addAttribute(FIELD_ENABLE_CAPTCHA, true);
}
}
}
session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
return LOGIN_PAGE;
}
//请求验证码图片
@GetMapping("/public/captcha.jpg")
public void captcha(HttpServletResponse response) {
captchaImageHelper.generateAndWriteCaptchaImage(response, SecurityConstants.SECURITY_KEY);
}
//获取当前用户的信息
@GetMapping("/user/self")
@ResponseBody
public Result test() {
CustomUserDetails details = DetailsHelper.getUserDetails();
return Results.successWithData(details);
}
}
④ 从 spring boot 官方文档可以得知,spring security 的核心配置都在 WebSecurityConfigurerAdapter 里,我们只需继承该适配器覆盖默认配置即可。首先来看看默认的登录页面以及如何配置登录页面。
通过 HttpSecurity 配置安全策略,首先开放了允许匿名访问的地址,除此之外都需要认证,通过 formLogin() 来启用表单登录,并配置了默认的登录页面,以及登录成功后的首页地址。
① 首先设计并创建系统用户表:
② CustomUserDetails
自定义 UserDetails,根据自己的需求将一些常用的用户信息封装到 UserDetails 中,便于快速获取用户信息,比如用户ID、昵称等。
package com.lyyzoo.sunny.core.userdetails;
import java.util.Collection;
import java.util.Objects;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
/**
* 定制的UserDetail对象
*
* @author bojiangzhou 2018/09/02
*/
public class CustomUserDetails extends User {
private static final long serialVersionUID = -4461471539260584625L;
private Long userId;
private String nickname;
private String language;
public CustomUserDetails(String username, String password, Long userId, String nickname, String language,
Collection<? extends GrantedAuthority> authorities) {
super(username, password, authorities);
this.userId = userId;
this.nickname = nickname;
this.language = language;
}
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
public String getLanguage() {
return language;
}
public void setLanguage(String language) {
this.language = language;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof CustomUserDetails)) {
return false;
}
if (!super.equals(o)) {
return false;
}
CustomUserDetails that = (CustomUserDetails) o;
if (!Objects.equals(userId, that.userId)) {
return false;
}
return false;
}
@Override
public int hashCode() {
int result = super.hashCode();
result = 31 * result + userId.hashCode();
result = 31 * result + nickname.hashCode();
result = 31 * result + language.hashCode();
return result;
}
}
③ CustomUserDetailsService
自定义 UserDetailsService 来从数据库获取用户信息,并将用户信息封装到 CustomUserDetails
package com.lyyzoo.sunny.security.core;
import java.util.ArrayList;
import java.util.Collection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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 com.lyyzoo.sunny.core.message.MessageAccessor;
import com.lyyzoo.sunny.core.userdetails.CustomUserDetails;
import com.lyyzoo.sunny.security.domain.entity.User;
import com.lyyzoo.sunny.security.domain.service.UserService;
/**
* 加载用户信息实现类
*
* @author bojiangzhou 2018/03/25
*/
@Component
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userService.getUserByUsername(username);
if (user == null) {
throw new UsernameNotFoundException(MessageAccessor.getMessage("login.username-or-password.error"));
}
Collection<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
//调用构造方法封装用户信息
return new CustomUserDetails(username, user.getPassword(), user.getId(),
user.getNickname(), user.getLanguage(), authorities);
}
}
UserServiceImpl:
package com.lyyzoo.sunny.security.domain.service.impl;
import javax.servlet.http.HttpServletRequest;
import com.lyyzoo.sunny.core.exception.CommonException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.social.connect.web.ProviderSignInUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.lyyzoo.sunny.core.base.BaseService;
import com.lyyzoo.sunny.security.domain.entity.User;
import com.lyyzoo.sunny.security.domain.mapper.UserMapper;
import com.lyyzoo.sunny.security.domain.service.UserService;
import org.springframework.web.context.request.ServletWebRequest;
/**
*
* @author bojiangzhou 2018/09/04
*/
@Service
public class UserServiceImpl extends BaseService<User> implements UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private ProviderSignInUtils providerSignInUtils;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public User getUserByUsername(String username) {
return userMapper.selectByUsername(username);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void loginFail(Long userId) {
User user = select(userId);
user.loginFail();
update(user);
}
@Override
public void loginSuccess(Long userId) {
User user = select(userId);
user.loginSuccess();
update(user);
}
@Override
public void bindProvider(String username, String password, HttpServletRequest request) {
// login
User user = select(User.FIELD_USERNAME, username);
if (user == null || !passwordEncoder.matches(password, user.getPassword())) {
throw new CommonException("user.error.login.username-or-password.error");
}
providerSignInUtils.doPostSignUp(user.getUserId().toString(), new ServletWebRequest(request));
}
}
④ CustomWebAuthenticationDetails
自定义 WebAuthenticationDetails 用于封装传入的验证码以及缓存的验证码,用于后续校验。
package com.lyyzoo.sunny.security.core;
import javax.servlet.http.HttpServletRequest;
import com.lyyzoo.sunny.captcha.CaptchaResult;
import org.springframework.security.web.authentication.WebAuthenticationDetails;
/**
* 封装验证码
*
* @author bojiangzhou 2018/09/18
*/
public class CustomWebAuthenticationDetails extends WebAuthenticationDetails {
public static final String FIELD_CACHE_CAPTCHA = "cacheCaptcha";
private String inputCaptcha; //输入的验证码
private String cacheCaptcha; //缓存中的验证码
public CustomWebAuthenticationDetails(HttpServletRequest request) {
super(request);
cacheCaptcha = (String) request.getAttribute(FIELD_CACHE_CAPTCHA);
inputCaptcha = request.getParameter(CaptchaResult.FIELD_CAPTCHA);
}
public String getInputCaptcha() {
return inputCaptcha;
}
public String getCacheCaptcha() {
return cacheCaptcha;
}
@Override
public boolean equals(Object object) {
if (this == object) {
return true;
}
if (object == null || getClass() != object.getClass()) {
return false;
}
if (!super.equals(object)) {
return false;
}
CustomWebAuthenticationDetails that = (CustomWebAuthenticationDetails) object;
return inputCaptcha != null ? inputCaptcha.equals(that.inputCaptcha) : that.inputCaptcha == null;
}
@Override
public int hashCode() {
int result = super.hashCode();
result = 31 * result + (inputCaptcha != null ? inputCaptcha.hashCode() : 0);
return result;
}
}
⑤ CustomAuthenticationDetailsSource
当然了,还需要一个构造验证码的 AuthenticationDetailsSource
package com.lyyzoo.sunny.security.core;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.web.authentication.WebAuthenticationDetails;
import org.springframework.stereotype.Component;
import com.lyyzoo.sunny.captcha.CaptchaImageHelper;
import com.lyyzoo.sunny.security.constant.SecurityConstants;
/**
* 自定义获取AuthenticationDetails 用于封装传进来的验证码
*
* @author bojiangzhou 2018/09/18
*/
@Component
public class CustomAuthenticationDetailsSource implements AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> {
@Autowired
private CaptchaImageHelper captchaImageHelper;
@Override
public WebAuthenticationDetails buildDetails(HttpServletRequest request) {
String cacheCaptcha = captchaImageHelper.getCaptcha(request, SecurityConstants.SECURITY_KEY);
request.setAttribute(CustomWebAuthenticationDetails.FIELD_CACHE_CAPTCHA, cacheCaptcha);
return new CustomWebAuthenticationDetails(request);
}
}
package com.lyyzoo.sunny.captcha;
import java.awt.image.BufferedImage;
import java.util.concurrent.TimeUnit;
import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.lyyzoo.sunny.captcha.autoconfigure.CaptchaProperties;
import com.lyyzoo.sunny.captcha.message.CaptchaMessageSource;
import com.lyyzoo.sunny.core.exception.MessageException;
import com.lyyzoo.sunny.core.redis.RedisHelper;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.util.WebUtils;
/**
* 图片验证码输出
*
* @author bojiangzhou 2018/08/08
*/
public class CaptchaImageHelper {
private static final Logger LOGGER = LoggerFactory.getLogger(CaptchaImageHelper.class);
@Autowired
private DefaultKaptcha captchaProducer;
@Autowired
private CaptchaProperties captchaProperties;
@Autowired
private RedisHelper redisHelper;
/**
* 生成验证码并输出图片到指定输出流,验证码的key为UUID,设置到Cookie中,key和验证码将缓存到Redis中
*
* @param response HttpServletResponse
* @param captchaCachePrefix 缓存验证码的前缀
*/
public void generateAndWriteCaptchaImage(HttpServletResponse response, String captchaCachePrefix) {
response.setDateHeader("Expires", 0);
response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
response.addHeader("Cache-Control", "post-check=0, pre-check=0");
response.setHeader("Pragma", "no-cache");
response.setContentType("image/jpeg");
ServletOutputStream out = null;
try {
String captcha = captchaProducer.createText();
String captchaKey = CaptchaGenerator.generateCaptchaKey();
Cookie cookie = new Cookie(CaptchaResult.FIELD_CAPTCHA_KEY, captchaKey);
cookie.setPath(StringUtils.defaultIfEmpty("/", "/"));
cookie.setMaxAge(-1);
response.addCookie(cookie);
// cache
redisHelper.strSet(captchaCachePrefix + ":captcha:" + captchaKey, captcha, captchaProperties.getImage().getExpire(), TimeUnit.MINUTES);
// output
BufferedImage bi = captchaProducer.createImage(captcha);
out = response.getOutputStream();
ImageIO.write(bi, "jpg", out);
out.flush();
} catch (Exception e) {
LOGGER.info("create captcha fail: {}", e);
} finally {
if (out != null) {
try {
out.close();
} catch (Exception e) {
LOGGER.info("captcha output close fail: {}", e);
}
}
}
}
/**
* 校验验证码
*
* @param request HttpServletRequest
* @param captcha captcha
* @param captchaCachePrefix captcha cache prefix
*/
public CaptchaResult checkCaptcha(HttpServletRequest request, String captcha, String captchaCachePrefix) {
Cookie captchaKeyCookie = WebUtils.getCookie(request, CaptchaResult.FIELD_CAPTCHA_KEY);
if (captchaKeyCookie == null) {
throw new MessageException("captcha key not null");
}
CaptchaResult captchaResult = new CaptchaResult();
if (StringUtils.isBlank(captcha)) {
captchaResult.setSuccess(false);
captchaResult.setMessage(CaptchaMessageSource.getMessage("captcha.validate.captcha.notnull"));
return captchaResult;
}
String captchaKey = captchaKeyCookie.getValue();
String cacheCaptcha = redisHelper.strGet(captchaCachePrefix + ":captcha:" + captchaKey);
redisHelper.delKey(captchaCachePrefix + ":captcha:" + captchaKey);
if (!StringUtils.equalsIgnoreCase(cacheCaptcha, captcha)) {
captchaResult.setSuccess(false);
captchaResult.setMessage(CaptchaMessageSource.getMessage("captcha.validate.captcha.incorrect"));
return captchaResult;
}
captchaResult.setSuccess(true);
return captchaResult;
}
/**
* 从request cookie 中获取验证码
*
* @param request HttpServletRequest
* @param captchaCachePrefix captcha cache prefix
*/
public String getCaptcha(HttpServletRequest request, String captchaCachePrefix) {
Cookie captchaKeyCookie = WebUtils.getCookie(request, CaptchaResult.FIELD_CAPTCHA_KEY);
if (captchaKeyCookie == null) {
return null;
}
String captchaKey = captchaKeyCookie.getValue();
String captcha = redisHelper.strGet(captchaCachePrefix + ":captcha:" + captchaKey);
redisHelper.delKey(captchaCachePrefix + ":captcha:" + captchaKey);
return captcha;
}
/**
* 获取验证码
*
* @param captchaCachePrefix 缓存前缀
* @param captchaKey 验证码KEY
* @return 验证码
*/
public String getCaptcha(String captchaCachePrefix, String captchaKey) {
String captcha = redisHelper.strGet(captchaCachePrefix + ":captcha:" + captchaKey);
redisHelper.delKey(captchaCachePrefix + ":captcha:" + captchaKey);
return captcha;
}
}
⑥ CustomAuthenticationProvider
自定义认证处理器,主要加入了验证码的检查,如果用户密码输入错误三次以上,则需要验证码。
//验证码工具类
package com.lyyzoo.sunny.security.core;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import com.lyyzoo.sunny.security.domain.entity.User;
import com.lyyzoo.sunny.security.domain.service.ConfigService;
import com.lyyzoo.sunny.security.domain.service.UserService;
/**
* 自定义认证器
*
* @author bojiangzhou 2018/09/09
*/
@Component
public class CustomAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
@Autowired
private UserService userService;
@Autowired
private CustomUserDetailsService detailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private ConfigService configService;
@Override
protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
// 如有其它逻辑处理,可在此处进行逻辑处理...
return detailsService.loadUserByUsername(username);
}
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
String username = userDetails.getUsername();
User user = userService.getUserByUsername(username);
// 检查验证码
if (authentication.getDetails() instanceof CustomWebAuthenticationDetails) {
if (configService.isEnableCaptcha(user.getPasswordErrorTime())) {
CustomWebAuthenticationDetails details = (CustomWebAuthenticationDetails) authentication.getDetails();
String inputCaptcha = details.getInputCaptcha();
String cacheCaptcha = details.getCacheCaptcha();
if (StringUtils.isEmpty(inputCaptcha) || !StringUtils.equalsIgnoreCase(inputCaptcha, cacheCaptcha)) {
throw new AuthenticationServiceException("login.captcha.error");
}
authentication.setDetails(null);
}
}
// 检查密码是否正确
String password = userDetails.getPassword();
String rawPassword = authentication.getCredentials().toString();
boolean match = passwordEncoder.matches(rawPassword, password);
if (!match) {
throw new BadCredentialsException("login.username-or-password.error");
}
}
}
⑦ CustomAuthenticationSuccessHandler
自定义认证成功处理器,用户认证成功,将密码错误次数置零。
package com.lyyzoo.sunny.security.core;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import com.lyyzoo.sunny.security.domain.entity.User;
import com.lyyzoo.sunny.security.domain.service.UserService;
/**
* 登录认证成功处理器
*
* @author bojiangzhou 2018/03/29
*/
@Component
public class CustomAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
@Autowired
private UserService userService;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
String username = request.getParameter("username");
User user = userService.getUserByUsername(username);
userService.loginSuccess(user.getId());
super.onAuthenticationSuccess(request, response, authentication);
}
}
⑧ CustomAuthenticationFailureHandler
用户认证失败,记录密码错误次数,并重定向到登录页面。
package com.lyyzoo.sunny.security.core;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.WebAttributes;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import com.lyyzoo.sunny.core.message.MessageAccessor;
import com.lyyzoo.sunny.security.config.SecurityProperties;
import com.lyyzoo.sunny.security.domain.entity.User;
import com.lyyzoo.sunny.security.domain.service.UserService;
/**
* 登录失败处理器
*
* @author bojiangzhou 2018/03/29
*/
@Component
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Autowired
private SecurityProperties securityProperties;
@Autowired
private UserService userService;
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
String username = request.getParameter("username");
//request.getSession(true):若存在会话则返回该会话,否则新建一个会话。
//request.getSession(false):若存在会话则返回该会话,否则返回NULL
//当向Session中存取登录信息时,一般建议:HttpSession session =request.getSession();
//当从Session中获取登录信息时,一般建议:HttpSession session =request.getSession(false);
HttpSession session = request.getSession(false);
if (session != null) {
session.setAttribute("username", username);
session.setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION,
MessageAccessor.getMessage(exception.getMessage(), exception.getMessage()));
}
if (exception instanceof BadCredentialsException) {
User user = userService.getUserByUsername(username);
userService.loginFail(user.getId());
}
redirectStrategy.sendRedirect(request, response, securityProperties.getLoginPage() + "?username=" + username);
}
}
⑨ 配置
前面的开发完成当然还需做配置,通过 formLogin() 来配置认证成功/失败处理器等。
通过 AuthenticationManagerBuilder 配置自定义的认证器。
SpringSecurity提供了一个 PasswordEncoder 接口用于处理加密解密。该接口有两个方法 encode 和 matches 。encode 对密码加密,matches 判断用户输入的密码和加密的密码(数据库密码)是否匹配。
package com.lyyzoo.sunny.security.config;
import com.lyyzoo.sunny.security.core.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
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 org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* Security 主配置器
*
* @author bojiangzhou
*/
@Configuration
@EnableConfigurationProperties(SecurityProperties.class)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private SecurityProperties properties;
@Autowired
private CustomAuthenticationDetailsSource authenticationDetailsSource;
@Autowired
private CustomAuthenticationProvider authenticationProvider;
@Autowired
private CustomAuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired
private CustomAuthenticationFailureHandler authenticationFailureHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/static/**", "/webjars/**", "/public/**", "/login", "/favicon.ico")
.permitAll() // 允许匿名访问的地址
.and() // 使用and()方法相当于XML标签的关闭,这样允许我们继续配置父类节点。
.authorizeRequests()
.anyRequest()
.authenticated() // 其它地址都需进行认证
.and()
.formLogin() // 启用表单登录
.loginPage(properties.getLoginPage()) // 登录页面
.defaultSuccessUrl("/index") // 默认的登录成功后的跳转地址
.authenticationDetailsSource(authenticationDetailsSource)
.successHandler(authenticationSuccessHandler)
.failureHandler(authenticationFailureHandler)
.and()
.csrf()
.disable()
;
}
/**
* 设置认证处理器
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider);
super.configure(auth);
}
/**
* 密码处理器
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
⑩ 登录页面
实现流程:对静态资源和login方法放行,进入登录首页输入用户密码,会进行用户名密码校验,如果错误则会记录错误次数,当达到三次则会开启验证码验证,会调用生成验证码工具类生成验证码图片,并重新对用户名密码和验证码进行校验,校验成功则返回前端成功的信息并把用户信息和权限带过去,跳到资源首页,失败则返回失败信息,重定向到登录页面。
其他的实现方式参考以下链接:
参考链接