在开发实际应用项目当中,肯定存在用户登录和授权的过程,之前我们使用自己开发的权限框架或者 Shiro 来做这块内容的扩展和延伸,今天使用 Spring 框架自身的权限框架来集成下,也就是 Spring Security。
Spring Security 核心功能包括以下三个部分:
1)认证,解决你是谁的问题,也即用户登录;
2)授权,解决你可以干什么的问题,并不是你登录就可以为所欲为;
3)攻击防护,解决防止别人伪造身份问题,
导入 springsecurity 的依赖包之后,我们的项目启动会自动开启基本安全认证,认证的用户名是 user,密码可以在控制台找到,形如
image
打开一个 URL,就可以看到需要登录验证
image
输入账号密码就可以正常使用。
当然这种情况是无法满足实际应用的,我们需要自己的用户名和密码来进行登录认证。
记下来首先配置一些 开启使用Springsecurity 的基本配置,在配置包里面新建一个类WebSecurityConfig,继承WebSecurityConfigurerAdapter,然后复写configure(HttpSecurity http)方法,具体代码如下
@Configurationpublic class WebSecurityConfig extends WebSecurityConfigurerAdapter{ @Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()//表单登录,身份认证
.and()
.authorizeRequests()//对请求授权
.anyRequest()//任何请求
.authenticated();//需要身份认证
}
}然后重启,进行访问,此时将会出现一个默认登录框,如下
image
账号密码和之前相同,可以测试,注意 URL 的跳转。
其实这个和之前没什么区别,也的确没什么区别,主要是原来默认的是http.httpBasic()改为了http.formLogin().
1.2基本原理其实 Springsecurity 就是一组过滤器,形成过滤器练,所有的访问请求都会经过这些过滤器,这些过滤器在系统启动的时候自行配置到链中,无需开发者关心。
过滤器链上有很多过滤器,其中比较主要的几个
1、UserPasswordAuthenticationFilter、baseicAuthenticationFilter 是认证用户身份的,每个过滤器就是一种验证方式。和上面对应的就是
http.httpBasic()---->baseicAuthenticationFilter
http.formLogin()---->UserPasswordAuthenticationFilter
这两个过滤器都是检查请求里面是否包含过滤器需要的信息。比如先经过UserPasswordAuthenticationFilter这个过滤器,那么就需要先查看是否是一个登陆请求,是否包含用户名和密码,如果没有这些信息,那么就到baseicAuthenticationFilter过滤器,会检查请求头里面是否包含需要的信息。
2、一旦通过认证,会有相应的标记进行记录,然后继续想后面的过滤器传递。最后到达链的终端是FilterSecurityInterceptor,他是整个过滤器链最终守门人,他决定该请求能否顺利的访问 Controller 里面的服务。也就是说前面的链不管结论如何都会走到最后整个过滤器,有他来决定是往后继续执行业务,还是抛出某个异常。
3、一旦有异常出现,会在倒数第二层的过滤器来处理这些异常,这个过滤器是ExceptionTranslationFilter。他会根据具体的异常会导向不同的页面。
image
上图中,除了第一类(绿色)的我们可以控制,第二第三(橙色和蓝色)是不可控制的,他们一定在过滤器链的末端。
深入源码
分别在绿色的 UserPasswordAuthenticationFilter,蓝色的异常处理过滤器,橙色的过滤器,以及自己的 Rest 服务 上打上断点。
首先在 Controller 的方法上打个断点
image
其次在FilterSecurityInterceptor的124行地方打断点
[图片上传失败...(image-c0c504-1527080640040)]
再次在ExceptionTranslationFilter 的123行的地方打上断点
image
最后在UsernamePasswordAuthenticationFilter的获取用户名和面膜的地方打上断点
image
然后我们开始运行一个请求,比如http://localhost:8888/users?username=123。
首先断点停在FilterSecurityInterceptor这个类,运维前面的过滤器对这个 URL 都不care,由于我们配置了所有请求都需要身份验证,那么这关肯定过不去的。在执行beforeInvocation的时候跑出一个异常,这个异常跑出来之后,被ExceptionTranslationFilter过滤器捕获到了。然后对异常的处理,处理结果是一个重定向到一个登陆页面。
接下来进行登录,这次停在UsernamePasswordAuthenticationFilter这个过滤器上,说明登录请求被诸葛过滤器抓住了,并且开始进行验证登录结果。
再继续就又到橙色的FilterSecurityInterceptor类上,其实这之间由个跳转的,登录 URL 处理完毕,正常登录后又回到http://localhost:8888/users?username=123这个请求,其实是这个请求走到了橙色过滤器上。一旦InterceptorStatusToken token = super.beforeInvocation(fi);执行完毕,就到实际业务代码里面了。
这部分功能被封装在UserDetailsService这个接口里面。这个接口只有一个方法
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
接受用户名,返回UserDetails对象。创建一个类,实现UserDetailsService,并且实现方法,代码实现如下,为了简便处理,忽略了数据层
@Componentpublic class CustomerUserDetailService implements UserDetailsService{ @Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //根据用户名去数据库查询用户信息
//可以注入 jdbc,mybatis 等 DAO
//这里方便演示,直接在代码里面做了
//User 对象已经实现了UserDetails
//AuthorityUtils.commaSeparatedStringToAuthorityList 方法是以逗号分割产生一个授权集合
User user=new User(username, "123456", AuthorityUtils.commaSeparatedStringToAuthorityList("admin")); return user;
}
}这下可以使用自己的用户登录逻辑了。
1.2.2校验逻辑主要是密码是否匹配,比如取出123456密码之后交给 Springsecurity 就可以了。
其次其他的一些校验,密码过期,用户冻结等。
主要看 UserDetails这个接口,里面包含了所有信息
image
后面4个布尔返回方法可以执行自己的校验逻辑
isAccountNonExpired----账号是否过期
isAccountNonLocked----账号是否冻结,可恢复
isCredentialsNonExpired----密码是否过期
isEnabled----账号是否删除,不可恢复
我们在构造 User 的时候使用有7参数的构造,改写
User user=new User(username, "123456",
true,//账号可用
true,//账号不过期
true,//密码不过期
true,//账号没有锁定
AuthorityUtils.commaSeparatedStringToAuthorityList("admin")); return user;1.2.3加密解密实际应用密码取出应该是一个加密的密码,而不是明文。Springsecurity 的密码加密在接口PasswordEncoder中处理。该接口有2个方法,分别是
String encode(CharSequence rawPassword);负责加密,用户注册的时候对明文加密,存到 DB。
boolean matches(CharSequence rawPassword, String encodedPassword);负责匹配
为了使用加密功能,这里使用一个美人的实现BCryptPasswordEncoder,把这个 bean 添加到配置中。
image
为了演示,这里直接使用
image
1、自定义用户登录页
之前 SpringSecurity 自带的登录页当然不能正常使用,我们需要自行定制一个,首先在配置里面增加一行登录页的名称
image
然后开始构建这个页面
image
需要注意的是:这个 login.html 需要排除请求认证之外。
image
这样我们就能使用自己的登录页面了。
image
登录的请求地址是/userlogin,form是2.2 校验短信验证码和登录
和用户密码登录类似构造自己的过滤器,但是短信验证码校验是放在过滤器之前,方便通用。
image
接下来开始逐个实现
SMSCodeAuthenticationToken模仿UsernamePasswordAuthenticationToken代码,稍作修改
//封装登录信息public class SMSCodeAuthenticationToken extends AbstractAuthenticationToken { private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; // ~ Instance fields
// ================================================================================================
private final Object principal;//未验证之前是手机号,验证之后是用户信息
//private Object credentials;//密码,由于在这之前已经验证过了,所以不需要这个属性
// ~ Constructors
// ===================================================================================================
public SMSCodeAuthenticationToken(String mobile) { super(null); this.principal = mobile;
setAuthenticated(false);
}
public SMSCodeAuthenticationToken(Object principal, Object credentials,
Collection extends GrantedAuthority> authorities) { super(authorities); this.principal = principal; super.setAuthenticated(true); // must use super, as we override
} // ~ Methods
// ========================================================================================================
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");
} super.setAuthenticated(false);
} @Override
public void eraseCredentials() { super.eraseCredentials();
} @Override
public Object getCredentials() { return null;
}
}SMSCodeAuthenticationFilter同样模仿UsernamePasswordAuthenticationFilter
public class SMSCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter { public static final String MOBILE = "mobile"; private String mobileParameter = MOBILE; private boolean postOnly = true; public SMSCodeAuthenticationFilter() { super(new AntPathRequestMatcher("/mobilelogin", "POST"));
} public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException { if (postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String mobile = obtainMobile(request); if (mobile == null) {
mobile = "";
}
mobile = mobile.trim();
SMSCodeAuthenticationToken authRequest = new SMSCodeAuthenticationToken(mobile); // Allow subclasses to set the "details" property
setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest);
} protected String obtainMobile(HttpServletRequest request) { return request.getParameter(mobileParameter);
} protected void setDetails(HttpServletRequest request, SMSCodeAuthenticationToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
} public void setMobileParameter(String mobileParameter) {
Assert.hasText(mobileParameter, "mobileParameter parameter must not be empty or null"); this.mobileParameter = mobileParameter;
} public void setPostonly(boolean postOnly) { this.postOnly = postOnly;
}
}SMSCodeAuthenticationProvider类的代码如下
public class SMSCodeAuthenticationProvider implements AuthenticationProvider{ private UserDetailsService userDetailsService;
//使用 UserDetailService 获取用户信息重新组装 Authentication
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
SMSCodeAuthenticationToken token=(SMSCodeAuthenticationToken)authentication;
UserDetails user=userDetailsService.loadUserByUsername((String)token.getPrincipal()); if(user==null){ throw new InternalAuthenticationServiceException("无法获取用户信息");
}
SMSCodeAuthenticationToken result=new SMSCodeAuthenticationToken(user,user.getAuthorities());
result.setDetails(token.getDetails());//需要把之前的 Detail 设置到新的 Token 里面
return result;
} //检查参数是不是我们定义的 SMSCodeAuthenticationToken
@Override
public boolean supports(Class> authentication) { return SMSCodeAuthenticationToken.class.isAssignableFrom(authentication);
} public UserDetailsService getUserDetailsService() { return userDetailsService;
} public void setUserDetailsService(UserDetailsService userDetailsService) { this.userDetailsService = userDetailsService;
}
}最后SMSCodeFilter雷同之前的图形验证码的过滤器,需要修改部分代码即可
//OncePerRequestFilter 保证过滤器每次只被调用一次public class SMSCodeFilter extends OncePerRequestFilter{ private AuthenticationFailureHandler authenticationFailureHandler;
private SessionStrategy sessionStrategy=new HttpSessionSessionStrategy();
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException { //判断拦截的请求 URL,只是登录的 URL
//并且是 POST 请求
if(StringUtils.equals("/mobilelogin", request.getRequestURI())
&& StringUtils.equalsIgnoreCase("post", request.getMethod())){ try {
validate(new ServletWebRequest(request));
} catch (ImageCodeException e) {//自定义异常进行捕获
//一旦出现异常,使用authenticationFailureHandler来处理
authenticationFailureHandler.onAuthenticationFailure(request, response, e); return;
}
}
filterChain.doFilter(request, response);
} private void validate(ServletWebRequest request) throws ServletRequestBindingException { //分别拿到 session 和请求里面的验证码信息
SMSCode codeInSession=(SMSCode)sessionStrategy.getAttribute(request, ValidateCodeController.SESSION_KEY_SMS);
String codeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(), "smsCode");
if(StringUtils.isEmpty(codeInRequest)){ throw new ImageCodeException("验证码不能为空");
}
if(codeInSession==null){ throw new ImageCodeException("验证码不存在");
}
if(codeInSession.isExpire()){
sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY_SMS); throw new ImageCodeException("验证码过期");
} if(!StringUtils.equals(codeInSession.getCode(), codeInRequest)){ throw new ImageCodeException("验证码不匹配");
} //清除 session 里面的 ImageCode
sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY_SMS);
} public AuthenticationFailureHandler getAuthenticationFailureHandler() { return authenticationFailureHandler;
} public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) { this.authenticationFailureHandler = authenticationFailureHandler;
} public SessionStrategy getSessionStrategy() { return sessionStrategy;
} public void setSessionStrategy(SessionStrategy sessionStrategy) { this.sessionStrategy = sessionStrategy;
}
}最终配置以上代码,使其可以正常工作
分连部分,首先配置前三个,配到核心包里面
@Componentpublic class SMSCodeAuthenticationConfig extends SecurityConfigurerAdapter{ @Autowired private AuthenticationFailureHandler myAuthenticationFailureHandler; @Autowired private AuthenticationSuccessHandler myAuthenticationSuccessHandler; @Autowired private UserDetailsService userDetailsService; @Override public void configure(HttpSecurity http) throws Exception { SMSCodeAuthenticationFilter sMSCodeAuthenticationFilter=new SMSCodeAuthenticationFilter(); sMSCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class)); sMSCodeAuthenticationFilter.setAuthenticationFailureHandler(myAuthenticationFailureHandler); sMSCodeAuthenticationFilter.setAuthenticationSuccessHandler(myAuthenticationSuccessHandler); SMSCodeAuthenticationProvider sMSCodeAuthenticationProvider=new SMSCodeAuthenticationProvider(); sMSCodeAuthenticationProvider.setUserDetailsService(userDetailsService); http.authenticationProvider(sMSCodeAuthenticationProvider) .addFilterAfter(sMSCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); } }
再把SMSCodeFilter类似ValidateCodeFilter在 SS 配置中进行
image
最后把SMSCodeAuthenticationConfig也添加到 SS 配智中。
先引入,最后 apply(sMSCodeAuthenticationConfig);
最后贴出完整代码
@Configurationpublic class WebSecurityConfig extends WebSecurityConfigurerAdapter{ @Autowired
private SecurityProperties securityProperties;
@Bean
public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder();
}
@Autowired
private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler; @Autowired
private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
@Autowired
private DataSource dataSource; @Bean
public PersistentTokenRepository persistentTokenRepository(){
JdbcTokenRepositoryImpl jdbcTokenRepositoryImpl=new JdbcTokenRepositoryImpl();
jdbcTokenRepositoryImpl.setDataSource(dataSource); //jdbcTokenRepositoryImpl.setCreateTableonStartup(true);//执行一次,创建表,也可以自行创建表
return jdbcTokenRepositoryImpl;
} @Autowired
private UserDetailsService userDetailsService;
@Autowired
private SMSCodeAuthenticationConfig sMSCodeAuthenticationConfig;
@Override
protected void configure(HttpSecurity http) throws Exception {
System.out.println("Config");
ValidateCodeFilter validateCodeFilter=new ValidateCodeFilter();
validateCodeFilter.setAuthenticationFailureHandler(myAuthenticationFailureHandler);
SMSCodeFilter smsCodeFilter=new SMSCodeFilter();
smsCodeFilter.setAuthenticationFailureHandler(myAuthenticationFailureHandler);
http
.addFilterBefore(smsCodeFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
.formLogin()//表单登录,身份认证
.loginPage("/authlogin")//设置登录页
.loginProcessingUrl("/userlogin")//设置表单提交请求的 URL
.successHandler(myAuthenticationSuccessHandler)
.failureHandler(myAuthenticationFailureHandler)
.and()
.rememberMe()
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(securityProperties.getWeb().getRememberMeSecond())
.userDetailsService(userDetailsService)
.and()
.authorizeRequests()//对请求授权
.antMatchers("/authlogin","/mobilelogin", "/login.html",//默认的登录页
"/image/code", "/sms/code",
securityProperties.getWeb().getLoginPage())
.permitAll()//表示对这个 url 永远的通过
.anyRequest()//任何请求
.authenticated()//需要身份认证
.and()
.csrf().disable()//把 csdf 跨站防护关闭
.apply(sMSCodeAuthenticationConfig);
}
}
作者:breezedancer
链接:https://www.jianshu.com/p/96e67ceb8fd8



