01-SpringSecurity认证、授权

一、工作原理

SpringSecurity是使用filter来对资源进行保护的,当初始化SpringSecurity时会创建一个 SpringSecurityFilterChain的过滤器链,它实现了servlet的filter,因此外部的请求会经过该链
在这里插入图片描述
![在这里插入图片描述](https://img-blog.csdnimg.cn/7880712652094db98577c48ba9ff66ee.png

默认状态下,这个filterChain是如下结构
在这里插入图片描述
在这里插入图片描述

二、认证流程

在这里插入图片描述
从上面的流程中我们可以看到几个重要的组件

  1. authenticationFilter:过滤器,将请求携带的参数封装成指定的token传给authenticationManager,可以为它设定成功和失败的处理器
  2. AuthenticationManager:认证管理,对项目中的AuthenticationProvider进行管理
  3. DaoAuthenticationProvider:自定义业务逻辑,对指定类型的token进行校验
  4. UserDetailsService:查询数据库中的用户信息,provider利用查询到的信息和token进行比对,决定是否认证成功
  5. SecurityContextHolder:通过认证的authentication会保存到安全上下文中,后续SpringSecurity需要校验权限时,会根据这个authentication来校验

三、组件

(一)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

  1. SpringSecurity的过滤器都是AbstractAuthenticationProcessingFilter的子类,它们的业务逻辑大致相同。一般来说,一个filter只针对特定的路径,比如SpringSecurity封装好的UsernamePasswordAuthenticationFilter,它只针对以/login结尾的请求,/login以外的请求并不会经过该filter。
  2. SpringSecurity是通过authentication实现类来完成后续授权判断的,因此当认证成果时我们需要执行 SecurityContextHolder.getContext().setAuthentication(authentication)将authentication交由SpringSecurity框架

(二)UserDetailService

  1. UserDetail是SpringSecurity中用来描述用户信息的接口,主要包括Username、password和permissionList
  2. 而UserDetailService则是需要我们实现的接口,需要实现loadUserByUsername(String username) 返回UserDetails信息,需要注意的是UserDetails必须包含权限列表(如果项目中有授权需求的话)

上述两个接口都需要我们在项目中实现

(三)自定义认证逻辑

我们不需要像之前一样设计token、provider、filter。而是像web项目一样自己写接口实现认证逻辑,主要流程如下:

  1. 根据用户传来的认证信息(username、phone…)传入loadUserByUsername方法中得到UserDetails
  2. 判断userDetails是否为null,若不为null则判断credentials是否正确,若上述条件任意一个不满足则返回登录失败
  3. 将UserDetals传入到token中(使用默认的usernamToken就行),执行SecurityContextHolder.getContext().setAuthentication(authentication);
  4. 其余业务操作…

具体实现可以看这篇博客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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值