Spring Security技术栈学习笔记(八)Spring Security的基本运行原理与个性化登录实现

正如你可能知道的两个应用程序的两个主要区域是“认证”和“授权”(或者访问控制)。这两个主要区域是Spring Security的两个目标。“认证”,是建立一个他声明的主题的过程(一个“主体”一般是指用户,设备或一些可以在你的应用程序中执行动作的其他系统)。“授权”指确定一个主体是否允许在你的应用程序执行一个动作的过程。为了抵达需要授权的店,主体的身份已经有认证过程建立。

一、Spring Security的基本原理

Spring Security的整个工作流程如下所示:
这里写图片描述
其中绿色部分的每一种过滤器代表着一种认证方式,主要工作检查当前请求有没有关于用户信息,如果当前的没有,就会跳入到下一个绿色的过滤器中,请求成功会打标记。绿色认证方式可以配置,比如短信认证,微信。比如如果我们不配置BasicAuthenticationFilter的话,那么它就不会生效。

FilterSecurityInterceptor过滤器是最后一个,它会决定当前的请求可不可以访问Controller,判断规则放在这个里面。当不通过时会把异常抛给在这个过滤器的前面的ExceptionTranslationFilter过滤器。

ExceptionTranslationFilter接收到异常信息时,将跳转页面引导用户进行认证。橘黄色和蓝色的位置不可更改。当没有认证的request进入过滤器链时,首先进入到FilterSecurityInterceptor,判断当前是否进行了认证,如果没有认证则进入到ExceptionTranslationFilter,进行抛出异常,然后跳转到认证页面(登录界面)。

二、自定义认证逻辑

Spring Security将用户信息的获取逻辑封装在一个接口里面,这个接口是UserDetailsService,这个接口只有一个方法:

UserDetails loadUserByUsername(String username) throws UsernameNotFoundException

这个方法需要传递一个参数,这个参数是username,通过username就可以去数据库查询用户信息,如果查询到,就可以将查询到的相关信息封装到UserDetail的一个实现类对象中,并返回,然后就可以交给Spring Security进行认证,如果没有查到,将抛出UsernameNotFoundException异常。返回的用户对象是User,它是org.springframework.security.core.userdetails.User提供的实体类,这个实体类有几个成员属性,分别是:

private String password;  // 第一个是从数据库中查询到的密码;
private final String username;  // 第二个是用户输入的用户名;
private final Set<GrantedAuthority> authorities;  // 第三个是授权列表;
private final boolean accountNonExpired;  // 第四个是当前账户是否过期;
private final boolean accountNonLocked;  // 第五个是账户是否被锁定;
private final boolean credentialsNonExpired;  // 第六个是账户的认证时间是否过期;
private final boolean enabled;  // 第七个是账户是否有效。

这个实体类有两个构造方法,分别是:

public User(String username, String password,
			Collection<? extends GrantedAuthority> authorities) {
		this(username, password, true, true, true, true, authorities);
	}

public User(String username, String password, boolean enabled,
			boolean accountNonExpired, boolean credentialsNonExpired,
			boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {

		if (((username == null) || "".equals(username)) || (password == null)) {
			throw new IllegalArgumentException(
					"Cannot pass null or empty values to constructor");
		}

		this.username = username;
		this.password = password;
		this.enabled = enabled;
		this.accountNonExpired = accountNonExpired;
		this.credentialsNonExpired = credentialsNonExpired;
		this.accountNonLocked = accountNonLocked;
		this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));
	}

对于自定义认证逻辑,这里提供可运行的代码:

package com.lemon.security.browser;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

/**
 * @author lemon
 * @date 2018/4/4 下午4:00
 */
@Component
@Slf4j
public class UserDetailsServiceImpl implements UserDetailsService {

    private final PasswordEncoder passwordEncoder;

    @Autowired
    public UserDetailsServiceImpl(PasswordEncoder passwordEncoder) {
        this.passwordEncoder = passwordEncoder;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        log.info("登陆用户名: {}", username);
        // 这里可以根据用户名到数据库中查询用户,获得数据库中得到的密码(这里不进行查询操作,使用固定代码)
        // 在实际的开发中,存到数据库的密码不是明文的,而是经过加密的
        String password = "123456";
        String encodedPassword = passwordEncoder.encode(password);
        log.info("加密后的密码为: {}", encodedPassword);
        // 这里查询该账户是否过期,这里使用固定代码,假设没有过期
        boolean accountNonExpired = true;
        // 这里查询该账户被删除,假设没有被删除
        boolean enabled = true;
        // 这里查询该账户认证是否过期,假设没有过期
        boolean credentialsNonExpired = true;
        // 查询该账户是否被锁定,假设没有被锁定
        boolean accountNonLocked = true;
        // 关于密码的加密,应该是在创建用户的时候进行的,这里仅仅是举例模拟
        return new User(username, encodedPassword,
                enabled, accountNonExpired,
                credentialsNonExpired, accountNonLocked,
                AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
    }
}

这里没有做数据库的查询操作,数据都是固定数据,也就是说输入任何用户名和指定的密码123456都是可以进行登录的。在实际的开发过程中,对于存入到数据库的密码,都是经过加密的,所以这里使用的固定密码假设是从数据库查询到的,然后对它进行加密。从数据库查询到的数据进行处理后封装到User的构造方法中,然后Spring Security就会将User对象和输入的密码进行比较,如果有任何问题,就会及时给前端进行提示。启动Spring Boot应用,访问任何API,比如http://localhost:8080/user,就会提示要求你输入密码。其中PasswordEncoder的实现类对象必须经过配置,如下所示:

/**
 * 配置了这个Bean以后,从前端传递过来的密码将被加密
 *
 * @return PasswordEncoder实现类对象
 */
@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

配置了这个Bean以后,从前端传递过来的密码就会被加密,所以从数据库查询到的密码必须是经过加密的,而这个过程都是在用户注册的时候进行加密的。这就合理解释了为什么对上面的代码进行加密了。

三、个性化用户认证流程

在实际的开发中,对于用户的登录认证,不可能使用Spring Security自带的方式或者页面,需要自己定制适用于项目的登录流程。这里要开发一个模块,支持用户在配置文件中配置自己的登录页面,如果用户配置了,则采用用户自己的页面,否则采用模块内置的登录页面。

1)自定义登录页面

对于用户自定义的登录行为,往往是登录后跳转或者是登录后返回提示用户签到等信息,开发者要编写一个类来继承WebSecurityConfigurerAdapter从而实现自定义的登录行为,并且要重写configure方法。这里先把代码贴出来,然后逐一说明。把这个类编写在项目lemon-security-browser中,定义一个包com.lemon.security.browser

package com.lemon.security.browser;

import com.lemon.security.core.properties.SecurityProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

/**
 * 浏览器安全验证的配置类
 *
 * @author lemon
 * @date 2018/4/3 下午7:35
 */
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {

    private final SecurityProperties securityProperties;
    private final AuthenticationSuccessHandler lemonAuthenticationSuccessHandler;
    private final AuthenticationFailureHandler lemonAuthenticationFailureHandler;

    @Autowired
    public BrowserSecurityConfig(SecurityProperties securityProperties, AuthenticationSuccessHandler lemonAuthenticationSuccessHandler, AuthenticationFailureHandler lemonAuthenticationFailureHandler) {
        this.securityProperties = securityProperties;
        this.lemonAuthenticationSuccessHandler = lemonAuthenticationSuccessHandler;
        this.lemonAuthenticationFailureHandler = lemonAuthenticationFailureHandler;
    }

    /**
     * 配置了这个Bean以后,从前端传递过来的密码将被加密
     *
     * @return PasswordEncoder实现类对象
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                .loginPage("/authentication/require")
                .loginProcessingUrl("/authentication/form")
                .successHandler(lemonAuthenticationSuccessHandler)
                .failureHandler(lemonAuthenticationFailureHandler)
                .and()
                .authorizeRequests()
                .antMatchers("/authentication/require", securityProperties.getBrowser().getLoginPage()).permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .csrf().disable();
    }
}

现在主要讲解重写的configure方法:

  • http.formLogin()指定的表单登录方式。

  • loginPage("/authentication/require")设置了登录页面,这里将URL指向了一个Controller,这个Controller可以根据用户的设置选择传递JSON数据还是返回一个登录页面。

  • loginProcessingUrl("/authentication/form")是更改了UsernamePasswordAuthenticationFilter默认的处理表单登录的/loginAPI,现在前端的form标签的action就可以写/authentication/form而不是固定的/login

  • successHandler(lemonAuthenticationSuccessHandler)指定了登录成功后的处理逻辑,一般都是跳转或者返回一个JSON数据。

  • failureHandler(lemonAuthenticationFailureHandler)指定了登录失败后的处理逻辑,一般是是跳转或者返回一个JSON数据。

  • antMatchers("/authentication/require", securityProperties.getBrowser().getLoginPage()).permitAll()意思是指/authentication/require和登录页面的请求无需验证权限。

  • csrf().disable()是指关闭跨站请求伪造的防护,这里是为了前期开发方便,关闭它。

整体描述:当用户访问系统的RESTful API的时候,第一次访问会检查当前访问的用户有没有权限访问,如果没有权限,就会进入到BrowserSecurityConfig的configure方法中,从而进入到/authentication/requireController方法中判断用户是否是访问HTML,如果是则跳转到登陆页面,否则返回一段JSON数据提示用户登录。这里还自定义配置了用户登陆成功和失败的处理逻辑,对于/authentication/require和登录页面的请求则无需验证权限,否则将陷进死循环中。

根据/authentication/require,我们编写一个Controller,来控制是跳转到登陆页面还是返回一段JSON,代码如下:

package com.lemon.security.browser;

import com.lemon.security.browser.support.SimpleResponse;
import com.lemon.security.core.properties.SecurityProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.savedrequest.SavedRequest;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author lemon
 * @date 2018/4/5 下午2:25
 */
@RestController
@Slf4j
public class BrowserSecurityController {

    private RequestCache requestCache = new HttpSessionRequestCache();

    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    private static final String HTML = ".html";

    private final SecurityProperties securityProperties;

    @Autowired
    public BrowserSecurityController(SecurityProperties securityProperties) {
        this.securityProperties = securityProperties;
    }

    /**
     * 当需要进行身份认证的时候跳转到此方法
     *
     * @param request  请求
     * @param response 响应
     * @return 将信息以JSON形式返回给前端
     */
    @RequestMapping("/authentication/require")
    @ResponseStatus(code = HttpStatus.UNAUTHORIZED)
    public SimpleResponse requireAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException {
        // 从session缓存中获取引发跳转的请求
        SavedRequest savedRequest = requestCache.getRequest(request, response);
        if (null != savedRequest) {
            String redirectUrl = savedRequest.getRedirectUrl();
            log.info("引发跳转的请求是:{}", redirectUrl);
            if (StringUtils.endsWithIgnoreCase(redirectUrl, HTML)) {
                // 如果是HTML请求,那么就直接跳转到HTML,不再执行后面的代码
                redirectStrategy.sendRedirect(request, response, securityProperties.getBrowser().getLoginPage());
            }
        }
        return new SimpleResponse("访问的服务需要身份认证,请引导用户到登录页面");
    }
}

当用户没有登录就访问某些API的时候,就会被引导进入此Controller,这里仅仅是模拟了用户如果是访问的HTML的话,就引导它到登录页面,如果是AJAX发送的请求的,往往需要返回JSON数据到前端。当用户访问的是HTML的时候,securityProperties.getBrowser().getLoginPage()就决定了用户是跳转到自定义的登录页面,还是此项目中自带的登录页面中。请看下面的配置类:

package com.lemon.security.core.properties;

import lombok.Data;

/**
 * @author lemon
 * @date 2018/4/5 下午3:08
 */
@Data
public class BrowserProperties {

    private String loginPage = "/login.html";

    private LoginType loginType = LoginType.JSON;
}

这里提供的是项目中自带的登录页面,在loginPage变量中给定了默认值,那么这个页面就在lemon-security-browserresourcesresources的文件夹内。对于自定义的登录页面,通过下面的代码从配置文件中读取:

package com.lemon.security.core.properties;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

/**
 * @author lemon
 * @date 2018/4/5 下午3:08
 */
@Data
@ConfigurationProperties(prefix = "com.lemon.security")
public class SecurityProperties {

    private BrowserProperties browser = new BrowserProperties();
}

为了使这个读取配置的类生效,需要写一个类:

package com.lemon.security.core;

import com.lemon.security.core.properties.SecurityProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;

/**
 * @author lemon
 * @date 2018/4/5 下午3:11
 */
@Configuration
@EnableConfigurationProperties(SecurityProperties.class)
public class SecurityCoreConfig {
}

以上代码基本完成了登录的基本功能,当用户访问的是HTML的时候,就会跳转到登录页面,如果是RESTful API的时候,返回一段JSON数据,前端可以根据JSON数据来提示用户登录。至于用户自定义界面,可以在application.yml配置,具体的配置如下:

# 配置自定义的登录页面
com:
  lemon:
    security:
      browser:
        loginPage: /lemon-login.html

2)自定义用户登录成功处理

用户登录成功后,Spring Security的默认处理方式是跳转到原来的链接上,这也是企业级开发的常见方式,但是有时候采用的是AJAX方式发送的请求,往往需要返回JSON数据,所以这里给出了简单的登录成功的案例:

package com.lemon.security.core.authentication;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.lemon.security.core.properties.LoginType;
import com.lemon.security.core.properties.SecurityProperties;
import lombok.extern.slf4j.Slf4j;
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 javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * {@link SavedRequestAwareAuthenticationSuccessHandler}是Spring Security默认的成功处理器
 *
 * @author lemon
 * @date 2018/4/5 下午7:42
 */
@Component("lemonAuthenticationSuccessHandler")
@Slf4j
public class LemonAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    private final ObjectMapper objectMapper;
    private final SecurityProperties securityProperties;

    @Autowired
    public LemonAuthenticationSuccessHandler(ObjectMapper objectMapper, SecurityProperties securityProperties) {
        this.objectMapper = objectMapper;
        this.securityProperties = securityProperties;
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        log.info("登录成功");
        if (LoginType.JSON.equals(securityProperties.getBrowser().getLoginType())) {
            // 如果用户自定义了处理成功后返回JSON(默认方式也是JSON),那么这里就返回JSON
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(objectMapper.writeValueAsString(authentication));
        } else {
            // 如果用户定义的是跳转,那么就使用父类方法进行跳转
            super.onAuthenticationSuccess(request, response, authentication);
        }
    }
}

SavedRequestAwareAuthenticationSuccessHandlerSpring Security默认的成功处理器,默认是跳转。这里将认证信息作为JSON数据进行了返回,也可以返回其他数据,这个是根据业务需求来定的,同样,这里也是配置了用户的自定义的登录类型,要么是跳转,要么是JSONsecurityProperties.getBrowser().getLoginType()决定了登录的类型,默认是JSON,如果需要跳转,也是需要在YAML配置文件中进行配置的。

# 配置自定义成功和错误处理方式
com:
  lemon:
    security:
      browser:
        loginType: REDIRECT

为了使自定义的成功处理器生效,需要在BrowserSecurityConfig中进行配置,前面的代码中已经进行了配置。

3)自定义用户登录失败处理

同样,如果登录失败,也需要自定义登录失败处理器,代码如下:

package com.lemon.security.core.authentication;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.lemon.security.core.properties.LoginType;
import com.lemon.security.core.properties.SecurityProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * {@link SimpleUrlAuthenticationFailureHandler}是Spring Boot默认的失败处理器
 *
 * @author lemon
 * @date 2018/4/5 下午7:51
 */
@Component("lemonAuthenticationFailureHandler")
@Slf4j
public class LemonAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    private final ObjectMapper objectMapper;
    private final SecurityProperties securityProperties;

    @Autowired
    public LemonAuthenticationFailureHandler(ObjectMapper objectMapper, SecurityProperties securityProperties) {
        this.objectMapper = objectMapper;
        this.securityProperties = securityProperties;
    }
    
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        log.info("登录失败");
        if (LoginType.JSON.equals(securityProperties.getBrowser().getLoginType())) {
            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(objectMapper.writeValueAsString(exception));
        } else {
            // 如果用户配置为跳转,则跳到Spring Boot默认的错误页面
            super.onAuthenticationFailure(request, response, exception);
        }
    }
}

配置方法和登录成功的方法一致。

Spring Security技术栈开发企业级认证与授权系列文章列表:

Spring Security技术栈学习笔记(一)环境搭建
Spring Security技术栈学习笔记(二)RESTful API详解
Spring Security技术栈学习笔记(三)表单校验以及自定义校验注解开发
Spring Security技术栈学习笔记(四)RESTful API服务异常处理
Spring Security技术栈学习笔记(五)使用Filter、Interceptor和AOP拦截REST服务
Spring Security技术栈学习笔记(六)使用REST方式处理文件服务
Spring Security技术栈学习笔记(七)使用Swagger自动生成API文档
Spring Security技术栈学习笔记(八)Spring Security的基本运行原理与个性化登录实现
Spring Security技术栈学习笔记(九)开发图形验证码接口
Spring Security技术栈学习笔记(十)开发记住我功能
Spring Security技术栈学习笔记(十一)开发短信验证码登录
Spring Security技术栈学习笔记(十二)将短信验证码验证方式集成到Spring Security
Spring Security技术栈学习笔记(十三)Spring Social集成第三方登录验证开发流程介绍
Spring Security技术栈学习笔记(十四)使用Spring Social集成QQ登录验证方式
Spring Security技术栈学习笔记(十五)解决Spring Social集成QQ登录后的注册问题
Spring Security技术栈学习笔记(十六)使用Spring Social集成微信登录验证方式

示例代码下载地址:

项目已经上传到码云,欢迎下载,内容所在文件夹为chapter008

更多干货分享,欢迎关注我的微信公众号:爪哇论剑(微信号:itlemon)
在这里插入图片描述

  • 3
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值