Spring 是一个非常流行和成功的 Java 应用开发框架。Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案。一般来说,Web 应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分。
对于上面提到的两种应用情景,Spring Security 框架都有很好的支持。在用户认证方面,Spring Security 框架支持主流的认证方式,包括 HTTP 基本认证、HTTP 表单验证、HTTP 摘要认证、OpenID 和 LDAP 等。在用户授权方面,Spring Security 提供了基于角色的访问控制和访问控制列表(Access Control List,ACL),可以对应用中的领域对象进行细粒度的控制。
//这个接口的实现类都是Token,称为票据,UsernamePasswordAuthenticationToken
//我们自定义实现就可以叫MobileCodeAuthenticationToken
public interface Authentication extends Principal, Serializable {
/**
* 权限列表
*/
Collection<? extends GrantedAuthority> getAuthorities();
/**
* 认证凭据,可能是密码,也可能是验证码,也可能是其他认证凭据
*/
Object getCredentials();
/**
* 认证请求的详细信息,可能是IP地址,也可能是认证序列号,也可能是null
*/
Object getDetails();
/**
* 如果通过认证,则返回的是包含(用户名和密码)或者(手机号和验证码)等的对象;如果认证不通过,
* 则返回的是用户名或者手机号等。
*/
Object getPrincipal();
/**
* 是否认证通过
*/
boolean isAuthenticated();
/**
* 修改认证状态
*/
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
Authentication接口就是用来携带认证信息的。认证信息包括用户身份信息,密码,及权限列表等
账号密码认证过滤器,用于认证用户信息,认证方式是由 AuthenticationManager 接口提供。
public interface AuthenticationManager {
/**
* 尝试认证传递过来的认证信息,如果认证成功,则会修改认证信息的状态。否则,则会抛出异常
*/
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
认证管理器,是认证相关的核心接口,也是发起认证的出发点。实际业务中可能根据不同的信息进行认证,所以Spring推荐通过实现 AuthenticationManager 接口来自定义自己的认证方式。Spring 提供了一个默认的实现 ProviderManager。
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
//省略其他属性
private List<AuthenticationProvider> providers = Collections.emptyList();
//省略其他内容
}
认证提供者管理器,该类中维护了一个认证提供者列表,只要这个列表中的任何一个认证提供者提供的认证方式认证通过,认证就结束。
public interface AuthenticationProvider {
/**
* 执行认证,并返回认证结果
*/
Authentication authenticate(Authentication authentication) throws AuthenticationException;
/**
* 支持认证的类型,用于实现自定义认证,比如手机号和短信登录认证需要用户自己来实现
*/
boolean supports(Class<?> authentication);
}
认证提供者,这是一个接口,具体如何认证,就看如何实现该接口。Spring Security 提供了 DaoAuthenticationProvider 实现该接口,这个类就是使用数据库中数据进行认证。
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
//省略其他属性
private PasswordEncoder passwordEncoder; //密码加密器,主要用于密码加密
private UserDetailsService userDetailsService; //用户详细信息服务,主要用于查询认证用户信息
//省略其他内容
}
public interface UserDetailsService {
/**
* 根据用户名获取用户详细信息
*/
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
public interface UserDetails extends Serializable {
/**
* 用户拥有的权限列表
*/
Collection<? extends GrantedAuthority> getAuthorities();
/**
* 密码
*/
String getPassword();
/**
* 账号
*/
String getUsername();
/**
* 账号是否过期,过期的账号不能进行认证
*/
boolean isAccountNonExpired();
/**
* 账号是否被锁定
*/
boolean isAccountNonLocked();
/**
* 凭据是否过期,过期的凭据不能进行认证
*/
boolean isCredentialsNonExpired();
/**
* 账号是否被启用,未启用的账号不能进行认证
*/
boolean isEnabled();
}
用户的详细信息,主要用于登录认证。
SecurityContextHolder 是最基本的对象,它负责存储当前 SecurityContext 信息。SecurityContextHolder默认使用 ThreadLocal 来存储认证信息,意味着这是一种与线程绑定的策略。在Web场景下的使用Spring Security,在用户登录时自动绑定认证信息到当前线程,在用户退出时,自动清除当前线程的认证信息。
SecurityContext 负责存储认证通过的用户信息(Authentication对象),保存着当前用户是什么,是否已经通过认证,拥有哪些权限等等。
AuthenticationSuccessHandler 主要用于认证成功后的处理,比如返回页面或者数据。
AuthenticationFailureHandler 主要用于认证失败后的处理,比如返回页面或者数据。
AccessDecisionManager 主要用于实现权限,决定请求是否具有访问的权限。
AccessDeniedHandler 主要用于无权访问时的处理
public class DelegatingFilterProxy extends GenericFilterBean {}
public abstract class GenericFilterBean implements Filter, BeanNameAware, EnvironmentAware,
EnvironmentCapable, ServletContextAware, InitializingBean, DisposableBean {}
由上面的类定义可以看出,DelegatingFilterProxy
是一个 Filter
,同时,也是一个InitializingBean
。
public interface InitializingBean {
/**
* 当Bean的所有属性设置后由包含的BeanFactory调用,而BeanFactory就是用来创建bean的,换言之,就是将Bean纳入Spring IOC容器
*/
void afterPropertiesSet() throws Exception;
}
实现了InitializingBean
接口的类,在创建对象并完成属性设置后,会被纳入Spring IOC 容器管理。如果DelegatingFilterProxy
在web.xml
中配置,那么,在容器启动时就会实例化该Filter
,然后完成初始化,随后被纳入 Spring IOC 容器管理。这样就相当于与 Spring 完成整合。
而 DelegatingFilterProxy
由 spring web 提供,与 Spring Security 无关。那么 DelegatingFilterProxy
到底有什么作用呢?
其作用是代理真正的Filter实现类
DelegatingFilterProxy
如何知道其所代理的Filter是哪个呢?
这是通过其自身的targetBeanName的属性来确定的,通过该名称,DelegatingFilterProxy
可以从WebApplicationContext
中获取指定的 bean
作为代理对象。该属性可以通过在web.xml
中定义 DelegatingFilterProxy
时通过 init-param
来指定,如果未指定,则将取其在web.xml
中声明时定义的名称作为 targetBeanName 的值。
<filter>
<filter-name>springSecurityFilterChainfilter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxyfilter-class>
<init-param>
<param-name>targetBeanNameparam-name>
<param-value>springSecurityFilterChainparam-value>
init-param>
filter>
<filter-mapping>
<filter-name>springSecurityFilterChainfilter-name>
<url-pattern>/*url-pattern>
filter-mapping>
使用Spring Security时,DelegatingFilterProxy 代理的就是一个 FilterChainProxy。当我们使用基于Spring Security的NameSpace进行配置时,系统会自动为我们注册一个名为 springSecurityFilterChain 类型为 FilterChainProxy 的 Bean,这也是为什么我们在使用 Spring Security 时需要在 web.xml
中声明一个 name 为 springSecurityFilterChain 的 DelegatingFilterProxy 的 Filter 了。
FilterChainProxy 有什么作用呢?
一个 FilterChainProxy 中可以包含有多个 FilterChain,但是某个请求只会对应一个 FilterChain,而一个 FilterChain 中又可以包含有多个 Filter。
而 Spring Security 底层正是通过一系列的 Filter 来工作的。具体详情如下:
将Security上下文与SpringWeb中用于处理异步请求映射的WebAsyncmanager进行集成
在每次请求处理之前将该请求相关的安全上下文信息加载到SecurityContextHolder中,然后在该次请求处理完成之后麻将SecurityContextHolder中关于这次请求的信息存储的‘仓储’中,然后将SecurityContextHolder中的信息清除,例如在Session中维护一个用户的安全信息就是这个过滤器处理的
用于将头信息加入响应中
用于处理跨站请求伪造
用于处理退出登录
[重点]
用于处理基于表单的登录请求,从表单中获取用户名和密码,默认情况下处理来自/login的请求,从表单中获取用户名和密码, 默认使用表单name值为username和password,这两个值可以通过这个过滤器的usernaemparamter个passwordParameter连个参数的值进行修改
如果没有配置登陆页面,那系统初始化就会配置这个过滤器。并且用于在需要进行登陆时生成一个登陆表单页
检测和处理http basic认证
用于处理请求的缓存
主要包装请求对象request
检测SecurityContextHolder中是否存在Authentication对象,如果不存在为其提供一个匿名Authentication
管理session的过滤器
处理AccessDeniedException和AuthenticationException异常
可以看作过滤器链的出口
当用户没有登录而直接访问资源时,从cookie中找出用户的信息,如果SpringSecurity能够识别出用户提供remember me cookie ,用户将不必填写用户名和密码,而是直接登录进入系统,该过滤器默认从不开启
package com.ch.authentication.config;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@EnableWebSecurity //启用security
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//创建密码加密器,并纳入Spring IOC容器管理,该Bean的名字就是方法名
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
//认证管理器配置
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//数据库数据认证提供器
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
//设置认证使用的用户详情服务,业就是查询用户信息的服务
daoAuthenticationProvider.setUserDetailsService();
//设置密码使用的加密器
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
//设置认证管理构建器使用的认证提供器
auth.authenticationProvider(daoAuthenticationProvider);
}
}
package com.ch.authentication.service;
import org.springframework.security.core.userdetails.UserDetailsService;
//用户业务层,继承了UserDetailsService,方便与security结合
public interface UserService extends UserDetailsService {
}
package com.ch.authentication.service.impl;
import com.ch.authentication.service.UserService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements UserService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return null;
}
}
package com.ch.authentication.model;
import lombok.Data;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.List;
@Data
public class User implements UserDetails {
private String username; //账号
private String password; //密码
private List<SimpleGrantedAuthority> authorities; //拥有的权限
@Override
public boolean isAccountNonExpired() { //账号是否未过期
return true;
}
@Override
public boolean isAccountNonLocked() { //账号是否未被锁定
return true;
}
@Override
public boolean isCredentialsNonExpired() {//凭据是否未过期
return true;
}
@Override
public boolean isEnabled() {//账号是否可用
return true;
}
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = new User();
user.setUsername("admin");
user.setPassword(passwordEncoder.encode("123456"));
user.setAuthorities(Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN"), new SimpleGrantedAuthority("ROLE_USER")));
return user;
}
//认证管理器配置
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//数据库数据认证提供器
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
//设置认证使用的用户详情服务,业就是查询用户信息的服务
daoAuthenticationProvider.setUserDetailsService(userService);
//设置密码使用的加密器
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
//设置认证管理构建器使用的认证提供器
auth.authenticationProvider(daoAuthenticationProvider);
}
package com.ch.authentication.config;
import com.ch.authentication.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@EnableWebSecurity //启用security
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserService userService;
//创建密码加密器,并纳入Spring IOC容器管理,该Bean的名字就是方法名
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
//认证管理器配置
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//数据库数据认证提供器
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
//设置认证使用的用户详情服务,业就是查询用户信息的服务
daoAuthenticationProvider.setUserDetailsService(userService);
//设置密码使用的加密器
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
//设置认证管理构建器使用的认证提供器
auth.authenticationProvider(daoAuthenticationProvider);
}
//Http认证配置
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();//关闭跨站请求模拟
//设置表单登录使用的登录地址、登录请求的URL地址、登录成功和失败分别使用的处理器 permitAll表示该操作不需要security的权限控制
http.formLogin().loginPage("/").loginProcessingUrl("/login")
.successHandler().failureHandler().permitAll();
http.authorizeRequests().anyRequest().authenticated();
//设置退出操作使当前session失效 permitAll表示该操作不需要security的权限控制
http.logout().invalidateHttpSession(true).permitAll();
}
}
package com.ch.authentication.handler;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
//认证成功的处理器
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.sendRedirect("/main.html");
}
}
package com.ch.authentication.handler;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
//认证失败的处理器
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.sendRedirect("/");
}
}
//Http认证配置
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();//关闭跨站请求模拟
//设置表单登录使用的登录地址、登录请求的URL地址、登录成功和失败分别使用的处理器 permitAll表示该操作不需要security的权限控制
http.formLogin().loginPage("/").loginProcessingUrl("/login")
.successHandler(loginSuccessHandler).failureHandler(loginFailureHandler).permitAll();
http.authorizeRequests().anyRequest().authenticated();
//设置退出操作使当前session失效 permitAll表示该操作不需要security的权限控制
http.logout().invalidateHttpSession(true).permitAll();
}
index.html
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Security登录title>
head>
<body>
<form action="login" method="post">
<input type="text" name="username">
<input type="password" name="password">
<input type="submit" value="登录">
form>
body>
html>
main.html
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Security登录成功title>
head>
<body>
认证通过了
body>
html>
登录请求被 UsernamePasswordAuthenticationFilter 拦截,该拦截器尝试认证,认证过程中调用 AuthenticationManager进行认证。AuthenticationManager进行认证时,将该认证管理器中的所有认证提供器遍历一遍,遍历过程中,首先检测认证提供器是否支持认证的票据类型,如果支持,则认证提供器开始进行认证。认证提供器认证过程中会调用 UserDetailsService 获取用户信息,然后进行信息比对,如果正确,则返回一个认证通过的票据。所有认证提供器中,只要任意一个认证提供器认证通过,则表示认证成功。
package com.ch.authentication.sms;
import org.springframework.lang.Nullable;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
//短信认证提供器,模仿UsernamePasswordAuthenticationFilter编写
public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
//短信登录使用的URL,请求类型必须时POST
private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/sms","POST");
public SmsAuthenticationFilter() {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
}
public SmsAuthenticationFilter(AuthenticationManager authenticationManager) {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
}
//尝试认证
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
return null;
}
//获取短信验证码
@Nullable
protected String obtainCode(HttpServletRequest request) {
return request.getParameter("code");
}
//获取手机号码
@Nullable
protected String obtainMobile(HttpServletRequest request) {
return request.getParameter("mobile");
}
}
package com.ch.authentication.sms;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;
import org.springframework.util.Assert;
import java.util.Collection;
//短信认证的票据
public class SmsAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
//认证之前存储手机号码,认证之后存储的是用户信息,也就是一个User对象
private final Object principal;
//验证码
private Object credentials;
/**
* 认证之前使用
* @param principal
*/
public SmsAuthenticationToken(Object principal, Object credentials){
super(null);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(false);
}
/**
* 认证之后使用
* @param principal
* @param authorities
*/
public SmsAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true);
}
@Override
public Object getCredentials() {
return credentials;
}
@Override
public Object getPrincipal() {
return principal;
}
//这个方法是security框架执行认证流程时调用的,用户不应该调用,应该使用构造方法完成认证
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
Assert.isTrue(!isAuthenticated,
"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
super.setAuthenticated(false);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
this.credentials = null;
}
}
package com.ch.authentication.sms;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SmsCode {
//手机号
private String mobile;
//验证码
private String code;
//过期时间
private long expire;
}
package com.ch.authentication.controller;
import com.ch.authentication.sms.SmsCode;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Random;
@Controller
public class SmsController {
@GetMapping("/code")
public void imgCode(@RequestParam("mobile")String mobile, HttpSession session, HttpServletResponse response) throws IOException {
BufferedImage bi = new BufferedImage(200, 40, BufferedImage.TYPE_INT_RGB);
Graphics graphics = bi.getGraphics();
graphics.setColor(Color.GRAY);
graphics.fillRect(0, 0, 200, 40);
StringBuilder builder = new StringBuilder();
Random r = new Random();
for(int i=0; i<6; i++){
int num = r.nextInt(10);
builder.append(num);
graphics.setColor(Color.red);
graphics.drawString(Integer.toString(num), i*10 + 20, 15);
}
//创建短信实体
SmsCode code = new SmsCode(mobile, builder.toString(), System.currentTimeMillis() + 5 * 60 * 1000);
//将短信实体放入session中
session.setAttribute("smsCode", code);
graphics.dispose();
ImageIO.write(bi, "jpg", response.getOutputStream());
}
}
//尝试认证
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
if(!request.getMethod().equalsIgnoreCase("POST")){
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String code = obtainCode(request);//获取短信验证码
//从session中获取发送的短信验证码信息
SmsCode smsCode = (SmsCode) request.getSession().getAttribute("smsCode");
if(code == null || smsCode == null){
throw new AuthenticationServiceException("SMS code cannot be null");
}
String mobile = obtainMobile(request);//获取手机号
long currentTime = System.currentTimeMillis();//获取系统当前时间
if(smsCode.getExpire() < currentTime || !mobile.equals(smsCode.getMobile())){//如果系统当前时间比验证码过期时间还要大,说明验证码过期,手机号码与验证码不匹配
throw new AuthenticationServiceException("SMS code is invalid:" + smsCode.getCode());
} else if(!code.equals(smsCode.getCode())){
throw new AuthenticationServiceException("SMS code error");
}
SmsAuthenticationToken token = new SmsAuthenticationToken(mobile, code);//创建SMS token
this.setDetails(request, token);
return this.getAuthenticationManager().authenticate(token);//调用认证管理器认证token
}
//将请求信息放入token中
protected void setDetails(HttpServletRequest request, SmsAuthenticationToken authRequest) {
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
package com.ch.authentication.sms;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
public class SmsAuthenticationProvider implements AuthenticationProvider {
//获取认证用户的信息的服务接口
private UserDetailsService userDetailsService;
/**
* 这个方法就是认证,如果没有抛出认证异常,说明认证成功
* @param authentication 未进行认证的信息,里面就是包含了一个mobile信息和请求的信息
* @return 返回一个认证完成的信息
* @throws AuthenticationException
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String mobile = (String) authentication.getPrincipal();
UserDetails userDetails = userDetailsService.loadUserByUsername(mobile); //通过手机号码获取用户信息
SmsAuthenticationToken authenticationToken = new SmsAuthenticationToken(userDetails, authentication.getCredentials(), userDetails.getAuthorities());
authenticationToken.setDetails(authentication.getDetails());
return authenticationToken;
}
@Override
public boolean supports(Class<?> authentication) {
// 判断 authentication 是不是 SmsCodeAuthenticationToken 类型或者其子类或者其子接口
return SmsAuthenticationToken.class.isAssignableFrom(authentication);
}
public UserDetailsService getUserDetailsService() {
return userDetailsService;
}
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
}
package com.ch.authentication.config;
import com.ch.authentication.handler.LoginFailureHandler;
import com.ch.authentication.handler.LoginSuccessHandler;
import com.ch.authentication.service.UserService;
import com.ch.authentication.sms.SmsAuthenticationFilter;
import com.ch.authentication.sms.SmsAuthenticationProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
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.EnableWebSecurity;
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.UsernamePasswordAuthenticationFilter;
@EnableWebSecurity //启用security
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserService userService;
@Autowired
private LoginSuccessHandler loginSuccessHandler;
@Autowired
private LoginFailureHandler loginFailureHandler;
@Autowired
@Qualifier("authenticationManagerBean") //表示使用指定名称的认证管理器
private AuthenticationManager authenticationManager;
//创建密码加密器,并纳入Spring IOC容器管理,该Bean的名字就是方法名
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
//认证管理器配置
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//创建短信认证提供器
SmsAuthenticationProvider smsAuthenticationProvider = new SmsAuthenticationProvider();
smsAuthenticationProvider.setUserDetailsService(userService);
//将认证提供器添加到认证管理器中
auth.authenticationProvider(smsAuthenticationProvider);
//数据库数据认证提供器
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
//设置认证使用的用户详情服务,业就是查询用户信息的服务
daoAuthenticationProvider.setUserDetailsService(userService);
//设置密码使用的加密器
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
//设置认证管理构建器使用的认证提供器
auth.authenticationProvider(daoAuthenticationProvider);
}
//Http认证配置
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();//关闭跨站请求模拟
//设置表单登录使用的登录地址、登录请求的URL地址、登录成功和失败分别使用的处理器 permitAll表示该操作不需要security的权限控制
http.formLogin().loginPage("/").loginProcessingUrl("/login")
.successHandler(loginSuccessHandler).failureHandler(loginFailureHandler).permitAll();
//设置获取验证码的请求放行
http.authorizeRequests().antMatchers(HttpMethod.GET, "/code").permitAll()
//其他请求需要认证
.anyRequest().authenticated();
//设置退出操作使当前session失效 permitAll表示该操作不需要security的权限控制
http.logout().invalidateHttpSession(true).permitAll();
//将短信认证过滤器添加账号密码过滤器的前面
http.addFilterBefore(smsAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public SmsAuthenticationFilter smsAuthenticationFilter() throws Exception {
SmsAuthenticationFilter smsAuthenticationFilter = new SmsAuthenticationFilter();
//设置短信认证过滤器使用的认证管理器
smsAuthenticationFilter.setAuthenticationManager(authenticationManager);
//设置登录成功的处理器
smsAuthenticationFilter.setAuthenticationSuccessHandler(loginSuccessHandler);
//设置登录失败的处理器
smsAuthenticationFilter.setAuthenticationFailureHandler(loginFailureHandler);
return smsAuthenticationFilter;
}
}
index.html
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Security登录title>
head>
<body>
<form action="login" method="post">
<input type="text" name="username">
<input type="password" name="password">
<input type="submit" value="登录">
form>
<form action="sms" method="post" >
<input type="text" name="mobile">
<input type="text" name="code">
<input type="button" value="获取验证码" onclick="getCode()">
<input type="submit" value="登录">
form>
body>
<script type="text/javascript">
function getCode(){
let elements = document.getElementsByName("code");
let img = document.createElement("img");
img.src = "code?mobile=" + document.getElementsByName("mobile")[0].value;
elements[0].after(img);
}
script>
html>
@EnableWebSecurity //启用security
//prePostEnabled = true启用@PreAuthorize()
//securedEnabled = true启用@Secured()
//jsr250Enabled = true启用@RolesAllowed、@PermitAll、@DenyAll 但该注解需要jar包支撑
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {}
JSR 250 依赖包
<dependency>
<groupId>javax.annotationgroupId>
<artifactId>jsr250-apiartifactId>
<version>1.0version>
dependency>
//Http认证配置
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();//关闭跨站请求模拟
//设置表单登录使用的登录地址、登录请求的URL地址、登录成功和失败分别使用的处理器 permitAll表示该操作不需要security的权限控制
http.formLogin().loginPage("/").loginProcessingUrl("/login")
.successHandler(loginSuccessHandler).failureHandler(loginFailureHandler).permitAll();
//设置获取验证码的请求放行
http.authorizeRequests().antMatchers(HttpMethod.GET, "/code").permitAll()
//授权请求表示任意请求都需要认证才能够访问
.anyRequest().authenticated();
//设置异常处理使用访问拒绝处理器
http.exceptionHandling().accessDeniedHandler();
//设置退出操作使当前session失效 permitAll表示该操作不需要security的权限控制
http.logout().invalidateHttpSession(true).permitAll();
//将短信认证过滤器添加账号密码过滤器的前面
http.addFilterBefore(smsAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
package com.ch.authentication.handler;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
//HTTP请求被拒绝的处理器
@Component
public class RequestDeniedHandler implements AccessDeniedHandler {
//这里就是拒绝处理的具体步骤实现
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setContentType("text/html;charset=utf-8");
//返回拒绝处理的信息
response.getWriter().print(accessDeniedException.getMessage());
}
}
@Autowired
private RequestDecisionManager decisionManager;
@Autowired
private RequestDeniedHandler deniedHandler;
//Http认证配置
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();//关闭跨站请求模拟
//设置表单登录使用的登录地址、登录请求的URL地址、登录成功和失败分别使用的处理器 permitAll表示该操作不需要security的权限控制
http.formLogin().loginPage("/").loginProcessingUrl("/login")
.successHandler(loginSuccessHandler).failureHandler(loginFailureHandler).permitAll();
//设置获取验证码的请求放行
http.authorizeRequests().antMatchers(HttpMethod.GET, "/code").permitAll()
//授权请求表示任意请求都需要认证才能够访问
.anyRequest().authenticated();
//设置异常处理使用访问拒绝处理器
http.exceptionHandling().accessDeniedHandler(deniedHandler);
//设置退出操作使当前session失效 permitAll表示该操作不需要security的权限控制
http.logout().invalidateHttpSession(true).permitAll();
//将短信认证过滤器添加账号密码过滤器的前面
http.addFilterBefore(smsAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
package com.ch.authentication.controller;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.security.RolesAllowed;
@RestController
public class TestController {
@GetMapping("/test1")
@PreAuthorize("hasRole('ROLE_ADMIN')")
public String test1(){
return "test1";
}
@GetMapping("/test2")
@PreAuthorize("hasRole('ROLE_TEST')")
public String test2(){
return "test2";
}
@GetMapping("/test3")
@Secured("ROLE_ADMIN")
public String test3(){
return "test3";
}
@GetMapping("/test4")
@Secured("ROLE_TEST")
public String test4(){
return "test4";
}
@GetMapping("/test5")
@RolesAllowed("ROLE_ADMIN")
public String test5(){
return "test5";
}
@GetMapping("/test6")
@RolesAllowed("ROLE_TEST")
public String test6(){
return "test6";
}
}
这个问题的根本原因在于登录结果的处理和拒绝访问的处理。如果能够判断一个请求是ajax请求,那么问题即将得到解决。
package com.ch.security.util;
import javax.servlet.http.HttpServletRequest;
//针对请求相关的操作工具类
public class RequestUtil {
private RequestUtil(){}
/**
* 验证请求是否是AJAX请求 这种验证对于jQuery发送的AJAX没有任何问题
* 但是,对于 axios发送的AJAX可能存在没有X-Requested-With这个头信
* 息的
* @param request
* @return
*/
public static boolean isAjaxRequest(HttpServletRequest request){
String header = request.getHeader("X-Requested-With");
return "XMLHttpRequest".equalsIgnoreCase(header);
}
}
登录的时候传递的参数在过滤器中获取不到,需要注意:在传递参数的时候要使用get方法传递参数的方式对参数进行拼接,然后赋值给data
$.ajax({
type: 'post',
url: 'login',
data: "username="+ $("#username").val() + "&password=" + $("#password").val(),
success: function (resp) {
console.log(resp);
}
});