正如你可能知道的两个应用程序的两个主要区域是“认证”和“授权”(或者访问控制)。这两个主要区域是
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
默认的处理表单登录的/login
的API
,现在前端的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/require
的Controller
方法中判断用户是否是访问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-browser
的resources
的resources
的文件夹内。对于自定义的登录页面,通过下面的代码从配置文件中读取:
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);
}
}
}
SavedRequestAwareAuthenticationSuccessHandler
是Spring Security
默认的成功处理器,默认是跳转。这里将认证信息作为JSON
数据进行了返回,也可以返回其他数据,这个是根据业务需求来定的,同样,这里也是配置了用户的自定义的登录类型,要么是跳转,要么是JSON
,securityProperties.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技术栈开发企业级认证与授权(二)使用Spring MVC开发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
。