在项目开发过程中,由于使用了依赖于SpringSecurity的框架,此框架集成了基于数据库的账号密码登录的功能,而且一直使用很正常,后来由于项目发展和扩充,需要通过手机号验证码进行登录与授权操作,那么在这个时候,突然发现,框架,虽然一直在使用,但是没有仔细学习过SpringSecurity的安全认证框架,对它的验证流程以及配置,并不了解,只能临时查看文档,参考已经实现的账号密码登录功能。进行学习和修改,从学习文档,到完成使用,大概经过了16个小时(2天)的时候,终于将所使用到的基本原理,以及登录进行了实现。下面将主要的实现步骤进行记录,以防忘记,并且做为回忆使用。
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
简单的Security工作流程
原有系统中已经配置完成,而且网上太多讲解如何开启配置的教程,这里只记录自己完成功能所需要的功能
1.增加了三个基础的配置文件,分别为:
Provider Token 以及ServiceImpl他们三个分别继承自:AuthenticationProvider ,AbstractAuthenticationToken和UserDetailsService
如果要自己写生成的用户详情,必须实现UserDetailsService接口,否则只能使用Security自带的,Security自带有,可实现账号和密码登录,但是不能实现自定义登录。
2.具体文件
OpenIdAuthenticationProvider.java
public class OpenIdAuthenticationProvider implements AuthenticationProvider {
private UserDetailsService userDetailsService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
OpenIdAuthenticationToken authenticationToken = (OpenIdAuthenticationToken) authentication;
String telephone = (String) authenticationToken.getPrincipal();
/**
* 验证码
*/
// String credentials = (String) authenticationToken.getCredentials();
UserDetails userDetails = userDetailsService.loadUserByUsername(telephone);
// 此时鉴权成功后,应当重新 new 一个拥有鉴权的 authenticationResult 返回
OpenIdAuthenticationToken authenticationResult = new OpenIdAuthenticationToken(userDetails,
userDetails.getAuthorities());
authenticationResult.setDetails(authenticationToken.getDetails());
return authenticationResult;
}
@Override
public boolean supports(Class<?> authentication) {
// 判断 authentication 是不是 SmsCodeAuthenticationToken 的子类或子接口
return OpenIdAuthenticationToken.class.isAssignableFrom(authentication);
}
public UserDetailsService getUserDetailsService() {
return userDetailsService;
}
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
}
OpenIdAuthenticationToken.java
public class OpenIdAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
/**
* 在 UsernamePasswordAuthenticationToken 中该字段代表登录的用户名, 在这里就代表登录的手机号码
*/
private final Object principal;
/**
* 构建一个没有鉴权的 SmsCodeAuthenticationToken
*/
public OpenIdAuthenticationToken(Object principal) {
super(null);
this.principal = principal;
setAuthenticated(false);
}
/**
* 构建拥有鉴权的 SmsCodeAuthenticationToken
*/
public OpenIdAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
// must use super, as we override
super.setAuthenticated(true);
}
@Override
public Object getCredentials() {
return null;
}
@Override
public Object getPrincipal() {
return this.principal;
}
@Override
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();
}
}
自定义此文件,可以重写接收到的数据, private final Object principal; 自定义以及private final Object credentials;
由于这里使用微信OpeId登录,所以只保留了第一个参数,没有使用第二个参数。
UserDetailsByOpenIdServiceImpl.java
@Service("userDetailsByOpenIdServiceImpl")
public class UserDetailsByOpenIdServiceImpl implements UserDetailsService {
private static final Logger log = LoggerFactory.getLogger(UserDetailsByOpenIdServiceImpl.class);
@Autowired
private IBaseConsignorLoginAccountService consignorAccountService;
@Override
public UserDetails loadUserByUsername(String openId) throws UsernameNotFoundException {
ConsignorUser user = consignorAccountService.queryByWeiXinMiniAppOPenId((openId));
if (StringUtils.isNull(user)) {
log.info("登录用户:{} 不存在.", openId);
throw new UsernameNotFoundException("未绑定用户或用户已停用");
}
return createLoginUser(user);
}
public UserDetails createLoginUser(ConsignorUser user) {
return new ConsignorLoginUser(IdUtil.randomUUID(), user.getId(), user);// permissionService.getMenuPermission(user));
}
}
注意:
@Service("userDetailsByOpenIdServiceImpl")
指定实例化的时候,所要实例化的名称,这里记录名称是为了在配置时使用这个名称,否则会提示已经实例化过UserDetailService。
3.Security配置
要继承WebSecurityConfigurerAdapter下面是几个关键代码
/**
* spring security配置
*/
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
*
*/
@Autowired
@Qualifier("userDetailsByOpenIdServiceImpl") //指定上面在@Service中定义的名称
private UserDetailsService userDetailsByOpenIdServiceImpl;
实例化:Provide
OpenIdAuthenticationProvider openIduthenticationProvider = new OpenIdAuthenticationProvider();
openIduthenticationProvider.setUserDetailsService(userDetailsByOpenIdServiceImpl);
进行绑定验证
// OPenId验证
httpSecurity.authenticationProvider(openIduthenticationProvider);
以上配置在configure函数中配置,只要重写此函数即可
主要内容如下
/**
* anyRequest | 匹配所有请求路径 access | SpringEl表达式结果为true时可以访问 anonymous | 匿名可以访问
* denyAll | 用户不能访问 fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录)
* hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问 hasAnyRole |
* 如果有参数,参数表示角色,则其中任何一个角色可以访问 hasAuthority | 如果有参数,参数表示权限,则其权限可以访问 hasIpAddress
* | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问 hasRole | 如果有参数,参数表示角色,则其角色可以访问 permitAll
* | 用户可以任意访问 rememberMe | 允许通过remember-me登录的用户访问 authenticated | 用户登录后可访问
*/
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
OpenIdAuthenticationProvider openIduthenticationProvider = new OpenIdAuthenticationProvider();
openIduthenticationProvider.setUserDetailsService(userDetailsByOpenIdServiceImpl);
httpSecurity
// CSRF禁用,因为不使用session
.csrf().disable()
// 认证失败处理类
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
// 基于token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 过滤请求
.authorizeRequests().antMatchers("/websocket/**").anonymous().antMatchers("/app/overt/**").anonymous()
.antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js").permitAll()
.antMatchers(securityProperties.getAnonymous()).anonymous()
.antMatchers(securityProperties.getPermitAll()).permitAll()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated().and().headers().frameOptions().disable();
httpSecurity.logout().logoutUrl(securityProperties.getLogoutUrl()).logoutSuccessHandler(logoutSuccessHandler);
// OPenId验证
httpSecurity.authenticationProvider(openIduthenticationProvider);
// 添加JWT filter
httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
// 添加CORS filter
httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);
4.JWT的使用
通过以上登录完成后,即可通过JWT包,返回JWT数据,
5.JWT的验证
JwtAuthenticationTokenFilter 继承自OncePerRequestFilter 实现JWT的认证
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
{
@Autowired
private TokenService tokenService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException
{
try
{
ConsignorLoginUser loginUser = tokenService.getLoginUser(request);
if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication())) {
tokenService.verifyToken(loginUser);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
chain.doFilter(request, response);
}
catch(RuntimeException ex)
{
ExceptionRequest.writeMessage(response, ex.getMessage(), 500);
}
}
}
通过Security获取到的信息,直接绑定到UsernamePasswordAuthenticationToken即可完成认证。
至此通过Security和JWT完成登录和认证