SpringSecurity+Oauth+短信登录+第三方登录认证+Session管理

零、前言

在开始本文之前,底层这块已经有了很大的调整,主要是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 简介

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() 来启用表单登录,并配置了默认的登录页面,以及登录成功后的首页地址。

在这里插入图片描述
默认请求/index后返回index首页面。

启动系统,访问资源跳转到自定义的登录页面了:
在这里插入图片描述

三、用户认证代码实现

① 首先设计并创建系统用户表:
在这里插入图片描述
② 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方法放行,进入登录首页输入用户密码,会进行用户名密码校验,如果错误则会记录错误次数,当达到三次则会开启验证码验证,会调用生成验证码工具类生成验证码图片,并重新对用户名密码和验证码进行校验,校验成功则返回前端成功的信息并把用户信息和权限带过去,跳到资源首页,失败则返回失败信息,重定向到登录页面。

其他的实现方式参考以下链接:

参考链接

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值