一、工作原理
SpringSecurity是使用filter来对资源进行保护的,当初始化SpringSecurity时会创建一个 SpringSecurityFilterChain的过滤器链,它实现了servlet的filter,因此外部的请求会经过该链
token
token是进行认证所需要的凭证,不同的authenticationProvider支持的token不一样,所以不同的认证方式同样对应着不同的token;比如使用账号密码和手机验证码使用的token就不一样
SpringSecurity为我们定义好了authentication的接口
Authentication
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
Object getCredentials();
Object getDetails();
Object getPrincipal();
boolean isAuthenticated();
void setAuthenticated(boolean var1) throws IllegalArgumentException;
}
UsernamePasswordAuthenticationToken(框架提供的)
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = 530L;
private final Object principal;
private Object credentials;
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super((Collection)null);
this.principal = principal;
this.credentials = credentials;
this.setAuthenticated(false);
}
public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true);
}
public Object getCredentials() {
return this.credentials;
}
public Object getPrincipal() {
return this.principal;
}
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
} else {
super.setAuthenticated(false);
}
}
public void eraseCredentials() {
super.eraseCredentials();
this.credentials = null;
}
}
PhoneCodeAuthenticationToken(自定义token)
public class PhoneCodeAuthenticationToken extends AbstractAuthenticationToken {
List<? extends GrantedAuthority> authorities;
Object credentials;
Object details;
Object principal;
public PhoneCodeAuthenticationToken(Object principal, Object credentials) {
super((Collection)null);
this.principal = principal;
this.credentials = credentials;
this.setAuthenticated(false);
}
public PhoneCodeAuthenticationToken(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;
}
}
(二)UserDetailsService
它的作用是查询用户信息,封装成UserDetais的实现类返回,在authenticationProvider中被使用到
官方定义接口
public interface UserDetailsService {
UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}
UserDetails(框架提供)
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
主要包括username、password、authorities三部分,用来表达当前用户的信息,当然我们可以使用User(框架提供)来构建UserDetails实现类也可以自定义UserDetails的实现类来包含更多的信息
(三)authenticationProvider
该类用来写token的认证逻辑
接口定义
public interface AuthenticationProvider {
//认证逻辑
Authentication authenticate(Authentication var1) throws AuthenticationException;
//支持token类型
boolean supports(Class<?> var1);
}
短信登录provider
public class SMSAuthenticationProvider implements AuthenticationProvider {
public static Map<String,String> codeMap = new HashMap<>();
private UserDetailsService userDetailsService;
public SMSAuthenticationProvider(UserDetailsService userDetailsService){
this.userDetailsService = userDetailsService;
}
/**
*
* @param authentication
* @return
* @throws AuthenticationException
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String phone = (String) authentication.getPrincipal();
String code = (String) authentication.getCredentials();
String cacheCode = codeMap.get(phone);
if(cacheCode == null || !code.equals(cacheCode)){
throw new InternalAuthenticationServiceException(
"code error");
}
SMSUserDetais details = (SMSUserDetais) userDetailsService.loadUserByUsername(phone);
if(details == null){
throw new UsernameNotFoundException("not found this phone");
}
List<? extends GrantedAuthority> authorities = new ArrayList<>();
return new PhoneCodeAuthenticationToken(details.getUsername(),details.getPassword(),authorities);
}
@Override
public boolean supports(Class<?> authentication) {
return PhoneCodeAuthenticationToken.class.isAssignableFrom(authentication);
}
public void setUserDetailsService(UserDetailsService userDetailsService){
this.userDetailsService = userDetailsService;
}
}
DaoAuthenticationProvider
框架本身也提供了一个provider,针对UsernamePasswordAuthenticationToken
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";
private PasswordEncoder passwordEncoder;
private volatile String userNotFoundEncodedPassword;
private UserDetailsService userDetailsService;
private UserDetailsPasswordService userDetailsPasswordService;
public DaoAuthenticationProvider() {
this.setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
}
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if (authentication.getCredentials() == null) {
this.logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
} else {
String presentedPassword = authentication.getCredentials().toString();
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
}
protected void doAfterPropertiesSet() {
Assert.notNull(this.userDetailsService, "A UserDetailsService must be set");
}
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
this.prepareTimingAttackProtection();
try {
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
} else {
return loadedUser;
}
} catch (UsernameNotFoundException var4) {
this.mitigateAgainstTimingAttack(authentication);
throw var4;
} catch (InternalAuthenticationServiceException var5) {
throw var5;
} catch (Exception var6) {
throw new InternalAuthenticationServiceException(var6.getMessage(), var6);
}
}
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
boolean upgradeEncoding = this.userDetailsPasswordService != null && this.passwordEncoder.upgradeEncoding(user.getPassword());
if (upgradeEncoding) {
String presentedPassword = authentication.getCredentials().toString();
String newPassword = this.passwordEncoder.encode(presentedPassword);
user = this.userDetailsPasswordService.updatePassword(user, newPassword);
}
return super.createSuccessAuthentication(principal, authentication, user);
}
private void prepareTimingAttackProtection() {
if (this.userNotFoundEncodedPassword == null) {
this.userNotFoundEncodedPassword = this.passwordEncoder.encode("userNotFoundPassword");
}
}
private void mitigateAgainstTimingAttack(UsernamePasswordAuthenticationToken authentication) {
if (authentication.getCredentials() != null) {
String presentedPassword = authentication.getCredentials().toString();
this.passwordEncoder.matches(presentedPassword, this.userNotFoundEncodedPassword);
}
}
public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
Assert.notNull(passwordEncoder, "passwordEncoder cannot be null");
this.passwordEncoder = passwordEncoder;
this.userNotFoundEncodedPassword = null;
}
protected PasswordEncoder getPasswordEncoder() {
return this.passwordEncoder;
}
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
protected UserDetailsService getUserDetailsService() {
return this.userDetailsService;
}
public void setUserDetailsPasswordService(UserDetailsPasswordService userDetailsPasswordService) {
this.userDetailsPasswordService = userDetailsPasswordService;
}
}
(四)filter
对符合条件的url进行过滤,框架提供了一个UsernamePasswordAuthenticationFilter处理账号密码登录,下面我们自定义一个处理验证码登录的filter
短信登录Filter
/**
* 短信登录过滤器
* @author liqi
*/
public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public static final String SMS_MOBILE = "phone";
public static final String CODE = "code";
public SmsAuthenticationFilter() {
super(new AntPathRequestMatcher("/login/sms", "POST"));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
String loginName = request.getParameter(SMS_MOBILE);
if (StringUtils.isEmpty(loginName)) {
throw new AuthenticationServiceException("手机号不能为空");
}
String code = request.getParameter(CODE);
if (StringUtils.isEmpty(code)) {
throw new AuthenticationServiceException("手机验证码不能为空");
}
PhoneCodeAuthenticationToken authRequest = new PhoneCodeAuthenticationToken(loginName,code);
authRequest.setDetails(super.authenticationDetailsSource.buildDetails(request));
System.out.println(authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
配置该filter
SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> securityConfigurerAdapter = new SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity>() {
@Override
public void configure(HttpSecurity httpSecurity) throws Exception {
// 手机号+短信登录
AuthenticationManager authenticationManager = httpSecurity.getSharedObject(AuthenticationManager.class);
AbstractAuthenticationProcessingFilter smsFilter = new SmsAuthenticationFilter();
smsFilter.setAuthenticationManager(authenticationManager);
smsFilter.setAuthenticationSuccessHandler(successHandler);
smsFilter.setAuthenticationFailureHandler(failureHandler);
httpSecurity.addFilterBefore(smsFilter, UsernamePasswordAuthenticationFilter.class);
}
};
http.apply(securityConfigurerAdapter);
四、整体配置
package com.codexie.security.config;
import com.codexie.security.filter.SmsAuthenticationFilter;
import com.codexie.security.provider.SMSAuthenticationProvider;
import com.codexie.security.service.impl.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
UserDetailsServiceImpl userDetailsService;
@Autowired
SuccessHandler successHandler;
@Autowired
DeniedHandler deniedHandler;
@Autowired
FailureHandler failureHandler;
public SecurityConfig() {
}
@Bean
public SMSAuthenticationProvider smsAuthenticationProvider(){
SMSAuthenticationProvider provider = new SMSAuthenticationProvider(userDetailsService);
return provider;
}
/**
* 添加自定义认证方式
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//将自定义的provider添加至list
auth.authenticationProvider(smsAuthenticationProvider())
//设置内置的provider的userDetailsService以及passwordEncoder
.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
//不定义没有password grant_type
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public DaoAuthenticationProvider daoAuthenticationProvider(){
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setUserDetailsService(userDetailsService);
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
return daoAuthenticationProvider;
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/public/**", "/webjars/**", "/v2/**", "/swagger**", "/static/**", "/resources/**");
//web.httpFirewall(new DefaultHttpFirewall());//StrictHttpFirewall 去除验url非法验证防火墙
}
@Bean
public PasswordEncoder passwordEncoder(){
return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/login*").permitAll()
.antMatchers("/login/code").permitAll()
.antMatchers("/logout*").permitAll()
.antMatchers("/druid/**").permitAll()
.anyRequest().authenticated()
.and().formLogin()
.loginPage("/login") // 登录页面
.loginProcessingUrl("/login.do") // 登录处理url
.failureUrl("/login?authentication_error=1")
.defaultSuccessUrl("/main")
.usernameParameter("username")
.passwordParameter("password")
.and().logout()
.logoutUrl("/logout.do")
.deleteCookies("JSESSIONID")
.logoutSuccessUrl("/")
.and().csrf().disable()
.exceptionHandling()
.accessDeniedPage("/login?authorization_error=2");
SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> securityConfigurerAdapter = new SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity>() {
@Override
public void configure(HttpSecurity httpSecurity) throws Exception {
// 手机号+短信登录
AuthenticationManager authenticationManager = httpSecurity.getSharedObject(AuthenticationManager.class);
AbstractAuthenticationProcessingFilter smsFilter = new SmsAuthenticationFilter();
smsFilter.setAuthenticationManager(authenticationManager);
smsFilter.setAuthenticationSuccessHandler(successHandler);
smsFilter.setAuthenticationFailureHandler(failureHandler);
httpSecurity.addFilterBefore(smsFilter, UsernamePasswordAuthenticationFilter.class);
}
};
http.apply(securityConfigurerAdapter);
}
}
五、授权
在实际开发中,我们往往会在controller接口上标明注解表达所需要的权限,当"经过认证"的用户访问这些受保护资源的时候,会按照图中的执行顺序执行方法
(一)基于注解授权
首先应当在配置类上增加@EnableGlobalMethodSecurity注解
@Secured
用于校验用户角色的注解
SpringSecurity中角色和权限放在一个容器中,角色前得加ROLE_来做区分,但若使用hasRole来判断角色则不用加该前缀
@Secured("ROLE_aaa") // 判断请求是否有这个角色
@RequestMapping("/toMain")
public String login() {
return "redirect:main.html";
}
@PreAuthorize
该注解即可校验权限也可校验角色,但必须开启@EnableGlobalMethodSecurity(prePostEnabled = true)
// 允许角色以 ROLE_ 开头,也可以不以 ROLE_ 开头,严格区分大小写
@PreAuthorize("hasRole('aaa')")
@RequestMapping("/toMain")
public String login() {
return "redirect:main.html";
}
@RestController
public class OrderController {
@GetMapping(value = "/r1")
@PreAuthorize("hasAuthority('p1')")//拥有p1权限方可访问此url
public String r1(){
//获取用户身份信息
UserDTO userDTO = (UserDTO) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return userDTO.getFullname()+"访问资源1";
}
}
六、经验技巧
(一)SpringSecurity过滤器
如果我们需要扩展项目的登录方式比如手机验证码登录,我们往往需要自定义token、filter并实现新的UserDetailService
- SpringSecurity的过滤器都是AbstractAuthenticationProcessingFilter的子类,它们的业务逻辑大致相同。一般来说,一个filter只针对特定的路径,比如SpringSecurity封装好的UsernamePasswordAuthenticationFilter,它只针对以/login结尾的请求,/login以外的请求并不会经过该filter。
- SpringSecurity是通过authentication实现类来完成后续授权判断的,因此当认证成果时我们需要执行 SecurityContextHolder.getContext().setAuthentication(authentication)将authentication交由SpringSecurity框架
(二)UserDetailService
- UserDetail是SpringSecurity中用来描述用户信息的接口,主要包括Username、password和permissionList
- 而UserDetailService则是需要我们实现的接口,需要实现loadUserByUsername(String username) 返回UserDetails信息,需要注意的是UserDetails必须包含权限列表(如果项目中有授权需求的话)
上述两个接口都需要我们在项目中实现
(三)自定义认证逻辑
我们不需要像之前一样设计token、provider、filter。而是像web项目一样自己写接口实现认证逻辑,主要流程如下:
- 根据用户传来的认证信息(username、phone…)传入loadUserByUsername方法中得到UserDetails
- 判断userDetails是否为null,若不为null则判断credentials是否正确,若上述条件任意一个不满足则返回登录失败
- 将UserDetals传入到token中(使用默认的usernamToken就行),执行SecurityContextHolder.getContext().setAuthentication(authentication);
- 其余业务操作…
具体实现可以看这篇博客https://www.macrozheng.com/mall/architect/mall_arch_05.html#%E7%99%BB%E5%BD%95%E6%B3%A8%E5%86%8C%E5%8A%9F%E8%83%BD%E5%AE%9E%E7%8E%B0