SpringSecurity 授权流程
简而言之,
默认的 用户名密码登录 就是 登录的时候 被对应的UsernamePasswordAuthenticationFilter拦截下来,然后通过前端传过来的Username,Password组成一个叫UsernamePasswordAuthenticationToken 的东西,这玩意基本可以看做是封装了用户名、密码、权限、SessionId 等信息的token,当然,在未验证的时候,他只有接受到用户名和密码。
紧接着 他会把这个token 传递给你写的 provider 进行验证,用户名密码登录的话会用实现的UserDetailService进行判断,验证通过则把 详细的user 信息 以及 权限信息放入 之前的 token 中,并且把Authentication信息设置成true,即标识为已被认证。
但是 , 在手机登录以及邮箱登录的时候,并没有用到用户名,密码等,之前默认的userDetailService 实现的是根据用户名查询,并不是手机和邮箱,也没有验证验证码的功能, 那么显然我们需要重新写 以下几个模块
1.EmailCodeAuthenticationToken
用于存放前端传来的信息
2.EmailCodeAuthenticationFilter
对特定手机登录URL进行过滤,封装token交由provider进行认证
3.EmailCodeAuthenticaitonProvider
认证token
4.successHandler、failHandler
分别对应 认证成功后跳转的位置和失败后跳转的位置
5.EmailCodeAuthenticationSecurityConfig
配置过滤器 以及 handler,provider
6 配置SpringSecurityconfig
1EmailCodeAuthenticationToken
package com.how2j.copy.security.token;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import javax.security.auth.Subject;
import java.util.Collection;
public class EmailCodeAuthenticationToken extends AbstractAuthenticationToken {
//这里的 principal 指的是 email 地址(未认证的时候)
private final Object principal;
public EmailCodeAuthenticationToken(Object principal) {
super((Collection)null);
this.principal = principal;
setAuthenticated(false);
}
public EmailCodeAuthenticationToken(Collection<? extends GrantedAuthority> authorities, Object principal) {
super(authorities);
this.principal = principal;
super.setAuthenticated(true);
}
//如果是Set认证状态,就无情的给一个异常,意思是:
//不要在这里设置已认证,不要在这里设置已认证,不要在这里设置已认证
//应该从构造方法里创建,别忘了要带上用户信息和权限列表哦
//原来如此,是避免犯错吧
/*
上面是复制的 接下来个人理解->
第一次 使用token 是未认证的 principal 里存放的是用户名
第二次 是认证的 该token 里面 还存放了 该用户的权限
springsecurity 通过这个标志来判断 是否被认证
*/
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);
}
}
/********************父类抽象方法***************************/
@Override
public boolean implies(Subject subject) {
return false;
}
@Override
public Object getCredentials() {
return null;
}
@Override
public Object getPrincipal() {
return this.principal;
}
}
2EmailCodeAuthenticationFilter
package com.how2j.copy.security.filter;
import com.how2j.copy.security.token.EmailCodeAuthenticationToken;
import org.springframework.security.authentication.AuthenticationServiceException;
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 org.springframework.security.web.util.matcher.RequestMatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
public class EmailCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
/**
* 前端传来的 参数名 - 用于request.getParameter 获取
*/
private final String DEFAULT_EMAIL_NAME="email";
private final String DEFAULT_EMAIL_CODE="e_code";
//是否 仅仅post
private boolean postOnly = true;
/**
* 父类中是 调用setFilterProcessesUrl(defaultFilterProcessesUrl); 方法创建可以通过的url
* 精确绑定url
* @param defaultFilterProcessesUrl
*/
public EmailCodeAuthenticationFilter(String defaultFilterProcessesUrl) {
super(defaultFilterProcessesUrl);
}
/**
* 通过 传入的 参数 创建 匹配器
* 即 Filter过滤的url
* @param requiresAuthenticationRequestMatcher
*/
public EmailCodeAuthenticationFilter(RequestMatcher requiresAuthenticationRequestMatcher) {
super(new AntPathRequestMatcher("/account/login/email","POST"));
}
/**
* 给权限
* filter 获得 用户名(邮箱) 和 密码(验证码) 装配到 token 上 ,
* 然后把token 交给 provider 进行授权
* @param httpServletResponse
* @return
* @throws AuthenticationException
* @throws IOException
* @throws ServletException
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse httpServletResponse) throws AuthenticationException, IOException, ServletException {
HttpSession session = request.getSession();
if(postOnly && !request.getMethod().equals("POST") ){
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}else{
String email = getEmail(request);
if(email == null){
email = "";
}
email = email.trim();
//如果 验证码不相等 故意让token出错 然后走springsecurity 错误的流程
boolean flag = isCodeEquals(request);
/*
封装 token
*/
EmailCodeAuthenticationToken token = null;
if(flag){
token = new EmailCodeAuthenticationToken(email);
}else{
token = new EmailCodeAuthenticationToken("error");
}
this.setDetails(request,token);
/*
交给 manager 发证
*/
return this.getAuthenticationManager().authenticate(token);
}
// return null;
}
/**
* 获取 头部信息 让合适的provider 来验证他
* @param request
* @param token
*/
public void setDetails(HttpServletRequest request , EmailCodeAuthenticationToken token ){
token.setDetails(this.authenticationDetailsSource.buildDetails(request ));
}
/**
* 获取 传来 的Email信息
*/
public String getEmail(HttpServletRequest request ){
String result= request.getParameter(DEFAULT_EMAIL_NAME);
return result;
}
/**
* 判断 传来的 验证码信息 以及 session 中的验证码信息
*/
public boolean isCodeEquals(HttpServletRequest request ){
HttpSession session = request.getSession();
String code1 = request.getParameter(DEFAULT_EMAIL_CODE);
System.out.println("code1**********"+code1);
String code2 = (String)session.getAttribute(DEFAULT_EMAIL_CODE+getEmail(request));
System.out.println("code2***********"+code2 );
try{
return code1.equals(code2);
}catch (Exception e){
return false;
}
}
}
3 EmailCodeAuthentictionProvider
package com.how2j.copy.security.provider;
import com.how2j.copy.security.token.EmailCodeAuthenticationToken;
import com.how2j.copy.service.UserService;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
public class EmailCodeAuthentictionProvider implements AuthenticationProvider {
UserService userService ;
public EmailCodeAuthentictionProvider(UserService userService) {
this.userService = userService;
}
/**
* 认证
* @param authentication
* @return
* @throws AuthenticationException
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
EmailCodeAuthenticationToken token = (EmailCodeAuthenticationToken)authentication;
UserDetails user = userService.getByEmail((String)token.getPrincipal());
System.out.println(token.getPrincipal());
if(user == null){
throw new InternalAuthenticationServiceException("无法获取用户信息");
}
System.out.println(user.getAuthorities());
UsernamePasswordAuthenticationToken result =
new UsernamePasswordAuthenticationToken(user,user.getPassword(),user.getAuthorities());
/*
Details 中包含了 ip地址、 sessionId 等等属性
*/
result.setDetails(token.getDetails());
return result;
}
@Override
public boolean supports(Class<?> aClass) {
return EmailCodeAuthenticationToken.class.isAssignableFrom(aClass);
}
}
4、handler
1
package com.how2j.copy.security.handler;
import org.springframework.context.annotation.Scope;
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;
/*
我这里加上scope 的原因是
spring中 autowired 装配的对象是单例的;
那么问题来了,
因为我 既有 邮箱登录 又有 密码登录,这两个我使用同一个handler ,
但是它们失败后跳转的url 又是不一样的,所以通过一个 tar 属性动态的绑定不同的handle
如果是单例的 , 那么后一个配置的tar 会 覆盖 前一个配置的tar ,
从而导致 邮件也好 , 手机也好 都跳转到 同一个位置,这是我们不想要的,
所以使用 多例 的@Scope 注解来保证绑定的是不同的handle
*/
@Component
@Scope("prototype")
public class MyAuthenticationFailHandler extends SimpleUrlAuthenticationFailureHandler {
/*
要跳转的位置
*/
String tar ;
public String getTar() {
return tar;
}
public void setTar(String tar) {
this.tar = tar;
}
/**
* 跳转
* @param request
* @param response
* @param exception
* @throws IOException
* @throws ServletException
*/
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
super.setDefaultFailureUrl("/account/login/"+tar+"?msg=error");
super.onAuthenticationFailure(request, response, exception);
}
}
2
package com.how2j.copy.security.handler;
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;
@Component
public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
super.setDefaultTargetUrl("/");
super.onAuthenticationSuccess(request, response, authentication);
}
}
5、EmailCodeAuthenticationSecurityConfig
package com.how2j.copy.security.config;
import com.how2j.copy.security.filter.EmailCodeAuthenticationFilter;
import com.how2j.copy.security.handler.MyAuthenticationFailHandler;
import com.how2j.copy.security.handler.MyAuthenticationSuccessHandler;
import com.how2j.copy.security.provider.EmailCodeAuthentictionProvider;
import com.how2j.copy.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.stereotype.Component;
@Component
public class EmailCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity>{
@Autowired
private MyAuthenticationFailHandler myAuthenticationFailHandler;
@Autowired
private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Autowired
private UserService userService ;
@Override
public void configure(HttpSecurity http) throws Exception {
super.configure(http);
myAuthenticationFailHandler.setTar("email");
EmailCodeAuthenticationFilter filter = new EmailCodeAuthenticationFilter(new AntPathRequestMatcher("/account/login/email","POST")) ;
filter .setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
filter .setAuthenticationSuccessHandler(myAuthenticationSuccessHandler);
filter .setAuthenticationFailureHandler(myAuthenticationFailHandler);
EmailCodeAuthentictionProvider provider = new EmailCodeAuthentictionProvider(userService) ;
http.authenticationProvider(provider)
.addFilterAfter(filter , UsernamePasswordAuthenticationFilter.class);
}
}
6、configure
protected void configure(HttpSecurity http) throws Exception {
// TODO Auto-generated method stub
http
.authorizeRequests()
.antMatchers("/css/**","/js/**","/fonts/**","/index")
.permitAll()
.antMatchers("/admin/**").hasRole("ADMIN")
.and()
.formLogin()
.loginPage("/account/login").failureUrl("/account/login?msg=error").defaultSuccessUrl("/")
.and()
.logout().logoutUrl("/logout").logoutSuccessUrl("/")
.and()
.rememberMe()
.and()
.exceptionHandling().accessDeniedPage("/403");
http.csrf().disable();
http.headers().frameOptions().sameOrigin();
http.apply(emailCodeAuthenticationSecurityConfig );
http.apply(smsCodeAuthenticationSecurityConfig );
}
只要加最后的apply即可