spring security使用数据库验证的逻辑处理
前面做了多个示例,包括使用jdbc和hibernate两种方式访问数据库获取用户信息和权限信息,其中一些关键步骤如下:
我们在SecurityConfig中配置覆盖configure方法时候,可以指定authenticationProvider,也可以不需要指定,直接指定userDetailsService。例如:
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { authenticationProvider.setPasswordEncoder(passwordEncoder()); auth.authenticationProvider(authenticationProvider); //auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); }
如果没有指定authenticationProvider,则security使用的是实现类DaoAuthenticationProvider。
如果指定自定义的authenticationProvider,为了方便,我们自定义的authenticationProvider也是继承自DaoAuthenticationProvider,只需要重写指定userDetailsService,authenticate方法,例如:
@Autowired @Qualifier("userDetailsService") @Override public void setUserDetailsService(UserDetailsService userDetailsService) { super.setUserDetailsService(userDetailsService); }
@Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { try { //调用上层验证逻辑 Authentication auth = super.authenticate(authentication); //如果验证通过登录成功则重置尝试次数, 否则抛出异常 userDetailsDao.resetFailAttempts(authentication.getName()); return auth; } catch (BadCredentialsException e) { //如果验证不通过,则更新尝试次数,当超过次数以后抛出账号锁定异常 userDetailsDao.updateFailAttempts(authentication.getName()); throw e; } catch (LockedException e){ //该用户已经被锁定,则进入这个异常 String error; UserAttempts userAttempts = userDetailsDao.getUserAttempts(authentication.getName()); if(userAttempts != null){ Date lastAttempts = userAttempts.getLastModified(); error = "用户已经被锁定,用户名 : " + authentication.getName() + "最后尝试登陆时间 : " + lastAttempts; }else{ error = e.getMessage(); } throw new LockedException(error); } }
在此方法中,仍然调用的是上层验证方法super.authenticate();在这里可以根据不同的验证异常抛出不同的异常,从而显示不同的用户账号状态,例如用户被锁定、用户失效、账号或者密码过期等,这里例子是多次登录失败锁定了用户。
下面我们看看security是如何验证账号的:
验证逻辑实现是在类AbstractUserDetailsAuthenticationProvider,此类实现了接口AuthenticationProvider的接口方法
Authentication authenticate(Authentication authentication) throws AuthenticationException;
实现方法中首先获取security定义的接口UserDetails,先从缓存userCache中获取,如果不存在,则调用方法retrieveUser。
retrieveUser的方法实现是在类DaoAuthenticationProvider,这个方法中可以看到
UserDetails loadedUser; try { loadedUser = this.getUserDetailsService().loadUserByUsername(username); } catch (UsernameNotFoundException notFound) { if (authentication.getCredentials() != null) { String presentedPassword = authentication.getCredentials().toString(); passwordEncoder.isPasswordValid(userNotFoundEncodedPassword, presentedPassword, null); } throw notFound; } .....
此处调用的是自定义的UserDetailsService中loadUserByUsername方法。于是可以看出自定义的UserDetailsService实现类关键是实现loadUserByUsername方法。
下面就两种方式的实现进行剖解:
1、使用jdbc方式时,我们自定义的UserDetailsService是继承了类JdbcDaoImpl,可以发现JdbcDaoImpl已经实现了接口UserDetailsService,实现了方法loadUserByUsername。
在实现方法中,关键是调用自己定义的两个方法loadUsersByUsername和createUserDetails。于是自定义的CustomUserDetailsService类只需要覆写这两个方法即可
@Override protected List<UserDetails> loadUsersByUsername(String username) { return getJdbcTemplate().query(super.getUsersByUsernameQuery(), new Object[]{username}, (rs, rowNum) -> { String username1 = rs.getString("username"); String password = rs.getString("password"); boolean enabled = rs.getBoolean("enabled"); boolean accountNonExpired = rs.getBoolean("accountNonExpired"); boolean credentialsNonExpired = rs.getBoolean("credentialsNonExpired"); boolean accountNonLocked = rs.getBoolean("accountNonLocked"); return new User(username1, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, AuthorityUtils.NO_AUTHORITIES); }); } @Override protected UserDetails createUserDetails(String username, UserDetails userFromUserQuery, List<GrantedAuthority> combinedAuthorities) { String returnUsername = userFromUserQuery.getUsername(); if (!super.isUsernameBasedPrimaryKey()) { returnUsername = username; } return new User(returnUsername, userFromUserQuery.getPassword(), userFromUserQuery.isEnabled(), userFromUserQuery.isAccountNonExpired(), userFromUserQuery.isCredentialsNonExpired(), userFromUserQuery.isAccountNonLocked(), combinedAuthorities); }
在这里我们根据需要指定各个值,例如用户名,密码,是否可用,账号和密码是否过期,是否账号被锁等,所以如果已经设计完成的数据表中字段名称不一致也没有关系,只要含义相同,获取值指定即可。
2、使用hibernate方式时,需要自己实现UserDetailsService接口中的方法loadUserByUsername:
@Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userDao.findByUserName(username); if (user == null) { throw new UsernameNotFoundException("该用户不存在:" + username); } List<GrantedAuthority> authorities = buildUserAuthority(user.getUserRole()); return buildUserForAuthentication(user, authorities); } // 把自定义的User转换成org.springframework.security.core.userdetails.User private org.springframework.security.core.userdetails.User buildUserForAuthentication( User user, List<GrantedAuthority> authorities) { return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), user.isEnabled(), user.isAccountNonExpired(), user.isCredentialsNonExpired(), user.isAccountNonLocked(), authorities); } private List<GrantedAuthority> buildUserAuthority(Set<UserRole> userRoles) { Set<GrantedAuthority> setAuths = new HashSet<>(); // Build user's authorities for (UserRole userRole : userRoles) { setAuths.add(new SimpleGrantedAuthority(userRole.getRole())); } return new ArrayList<>(setAuths); }
在方法中使用hibernate的方式获取自定义的User实例,然后转换成security中的org.springframework.security.core.userdetails.User即可,org.springframework.security.core.userdetails.User是接口UserDetails的实现类。
附上所有示例的代码的github地址: https://github.com/hongxf1990/spring-security-learning
嘿嘿,如果觉得以上实例项目中可以借鉴的话,不妨打个赏吧