架构小白到砖家-17-【密码安全问题】-springsecurity整合密码加密机制BCryptPasswordEncoder

时间:2019-01-17
本文章向大家介绍架构小白到砖家-17-【密码安全问题】-springsecurity整合密码加密机制BCryptPasswordEncoder,主要包括架构小白到砖家-17-【密码安全问题】-springsecurity整合密码加密机制BCryptPasswordEncoder使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

用户认证需要验证用户的密码是否正确,但是为了保护用户隐私,防止用户账号密码泄露,用户在注册的时候,都需要将密码进行加密。那么在用户认证的时候,就需要将原始密码和加密密码进行验证。这样就需要注册加密和验证加密的方式保存一致。

我们就补充下加密的概念,前面咱们已经在数据库密码加密时,学习了对称加密和非对称加密的概念,知道了私钥和公钥的作用。不管是对称还是非对称加密,都是可以将密码还原成真实信息。用户的密码肯定不希望被别人知道,所以咱们需要一种单向加密的算法,只能加密,无法还原加密前的信息。
一般咱们使用的MD5加密算法,这种方式如果大家都知道加密算法,也是有可能被暴力破解的。所以人们又想到在加密的时候,添加一个自定义的特殊“盐”,这样暴力破解就很难了。

所以我们用户密码加密,需要一个单向的加密算法。
在springsecurity中,提供了新老两种加密方式,但是接口名称都叫PasswordEncoder。这就让人很头大了,security又为了兼容,所以导致本来简单的添加加密实现,变得复杂了。我们就来看看这两个加密接口的区别。

老接口:
org.springframework.security.authentication.encoding.PasswordEncoder

@Deprecated
public interface PasswordEncoder {
	String encodePassword(String arg0, Object arg1);
	boolean isPasswordValid(String arg0, String arg1, Object arg2);
}

新接口:
org.springframework.security.crypto.password.PasswordEncoder

public interface PasswordEncoder {
	String encode(CharSequence arg0);
	boolean matches(CharSequence arg0, String arg1);
}

我们可以看到两个接口,就两个方法,一个方法用来加密,一个方法用来验证密码。除了方法名称变了,新接口别老接口少了一个Object 参数,这个参数就是“盐”。

具体的理论我们就不深究了,大概的意思就是,新接口已经采用了随机“盐”概念,不需要再自己指定一个盐了。

所以咱们这里就使用新接口的实现类BCryptPasswordEncoder就可以了。

接下来,咱们就开始security的加密整合吧。
用户认证流程,咱们前面并没有研究,只是知道自己实现一个UserDetailsService接口,并将对象托管给spring。剩下的就交给security自己去识别使用了。

现在需要增加认证的加密机制,本来很简单,只需要在配置类WebSecurityConfigurerAdapter中重写认证配置方法,把加密工具对象给builder就可以了。

	@Override
	protected void configure(AuthenticationManagerBuilder auth)	throws Exception {
		super.configure(auth);
		auth.userDetailsService(getUserDetailsService()).passwordEncoder(new BCryptPasswordEncoder());
	}

但是现在有两个都叫PasswordEncoder的加密接口,把事情搞大了。
这么处理怎么都不行,所以咱们还是得研究整个用户认证流程。

所以我们需要的工作,
第一,手动初始化DaoAuthenticationProvider对象,把PasswordEncoder和UserDetailsService注入进入。
第二,UserDetailsService中的用户密码替换为加密后的密码。

先用BCryptPasswordEncoder生成加密密码

	@Test
	public void test2() throws Exception{
		//密码
		String password = "123456";	     
//		String password = "123";	     
		System.out.println("加密前的密码:"+password);
		
		//加密后的密文
		BCryptPasswordEncoder encoder =  new BCryptPasswordEncoder();
		String password2 = encoder.encode(password);
		String password3= "$2a$10$.hZRd4YGFHVlMRRSrIg2ou7WQr2gt4oA/NPBcrjM/KTgiXiFEN4g.";//123456
//		String password3= "$2a$10$ttZd.w89KMw5OiKxxDZ/LuMiEWZ8V3wk.p6mVa14xCHte0sLy1pNe";//123
		
		System.out.println("加密后的密码:"+password2);  
		System.out.println("密码是否匹配:"+encoder.matches(password, password2));
		System.out.println("密码是否匹配:"+encoder.matches(password, password3));
	}

第一,手动初始化DaoAuthenticationProvider对象

第二,UserDetailsService中的用户密码替换为加密后的密码。

回顾总结
用户密码使用单向加密算法,security有两个PasswordEncoder,使用最新随机盐方式的BCryptPasswordEncoder。了解security的用户认证流程,认证方式很多,可以找到对应的AuthenticationProvider。

补充下源码

WebSecurityConfig

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
	
	/**
	 * 为了验证密码机密机制,
	 * 这里手动初始化了WebUserDetailsService、AuthenticationProvider
	 * 另外还需要使用重写父类方法configure(AuthenticationManagerBuilder auth)
	 * auth.userDetailsService(getUserDetailsService());
	 */
	@Bean
	public WebUserDetailsService getUserDetailsService() {
		return new WebUserDetailsService();
	}
	
	@Bean
	public AuthenticationProvider getAuthenticationProvider(){
	    DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
	    authenticationProvider.setUserDetailsService(userDetailsService());
	    authenticationProvider.setPasswordEncoder(new PasswordEncoder(){
			
			public String encodePassword(String rawPassword, Object arg1) {
				return new BCryptPasswordEncoder().encode(rawPassword);
			}

			public boolean isPasswordValid(String encodedPassword, String rawPassword, Object arg2) {
				return new BCryptPasswordEncoder().matches(rawPassword, encodedPassword);
			}
			
		});
	    return authenticationProvider;
	}
	
	@Override
	protected void configure(AuthenticationManagerBuilder auth)	throws Exception {
		super.configure(auth);
		auth.userDetailsService(getUserDetailsService());
	}

	
	/**
	 * 创建自定义用户授权拦截器,MySecurityInterceptor;
	 * 这里不能将自定义拦截器托管个spring,否则会重复拦截;
	 * 只能手动创建对象,并且插入到FilterSecurityInterceptor之前。
	 * .addFilterBefore(getMySecurityInterceptor(), FilterSecurityInterceptor.class);
	 */
	@Autowired
	private MyAccessDecisionManager myAccessDecisionManager;
	@Autowired
	private FilterInvocationSecurityMetadataSource securityMetadataSource;
	
	MySecurityInterceptor getMySecurityInterceptor(){
		MySecurityInterceptor ms = new MySecurityInterceptor(myAccessDecisionManager);
		ms.setSecurityMetadataSource(securityMetadataSource);
		return ms;
	}
	
	
	protected void configure(HttpSecurity http) throws Exception {
	    http
	    		//URL权限控制,优先级从上到下
	    		.authorizeRequests()
		        .antMatchers("/easyui/**","/**/*.js","/**/*.css","/**/*.png").permitAll()      //无需授权的资源     
	            .antMatchers("/test/admin/**").hasRole("ADMIN")		//指定角色授权,直接使用hasRole方法                                    
	            .antMatchers("/test/web/**").access("hasRole('ADMIN') or hasRole('NORMAL')")  //指定角色授权,使用access方法      
	            .anyRequest().authenticated()	//默认所有资源都需要授权
	            
	           //自定义登录逻辑
		        .and()
		        .formLogin()
	            .loginPage("/login.html") 	//自定义登录页面
	            .loginProcessingUrl("/login")  //登录请求,默认url是/login
//	            .successForwardUrl("successForwardUrl")	//登录成功跳转地址
//	            .failureForwardUrl("failureForwardUrl")		//登录失败跳转地址
	            .permitAll()     //允许登录相关的请求,所有人可以访问
		            
	            //自定义注销逻辑
		    	.and()
		    	.logout()
//		        .logoutUrl("/my/logout")     //默认url是/logout                                           
	            .logoutSuccessUrl("/")      //注销成功后,跳转指定页面                                     
	            .invalidateHttpSession(true)    
	            
	            //关闭额外安全机制
	            .and()
	            .csrf().disable()//关闭csrf防跨站请求伪造机制,不然ajax访问无法使用
	           .headers().frameOptions().disable();//关闭iframe防攻击机制,不然iframe无法调用URL
	    
	    //添加自定义的拦截器到security拦截链中
	    http
	    	//自定义用户授权拦截器,插入到FilterSecurityInterceptor之前。
	    	.addFilterBefore(getMySecurityInterceptor(), FilterSecurityInterceptor.class);
	}
	
}

WebUserDetailsService

public class WebUserDetailsService implements UserDetailsService {

	private final static Logger logger = LoggerFactory.getLogger(WebUserDetailsService.class); 
	
	public UserDetails loadUserByUsername(String username)
			throws UsernameNotFoundException {
		
		logger.info("用户登录:"+username);	
		
		if (StringUtils.isEmpty(username)) {
			return null;
		}else if("admin".equals(username) ){
//			UserDetails user = User.withUsername(username).password("123456").roles("ADMIN","NORMAL").build();
			//这里使用自定义权限列表的方式初始化权限
			List<GrantedAuthority> grantedAuthorities = new ArrayList<GrantedAuthority> ();
			grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
			grantedAuthorities.add(new SimpleGrantedAuthority("delete"));
//			UserDetails user = new User(username,"123456",grantedAuthorities);
			UserDetails user = new User(username,"$2a$10$.hZRd4YGFHVlMRRSrIg2ou7WQr2gt4oA/NPBcrjM/KTgiXiFEN4g.",grantedAuthorities);
	        return user;
		}else{
//			UserDetails user = User.withUsername(username).password("123").roles("NORMAL").build();
			//这里使用自定义权限列表的方式初始化权限
			List<GrantedAuthority> grantedAuthorities = new ArrayList<GrantedAuthority> ();
			grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_NORMAL"));
//			UserDetails user = new User(username,"123",grantedAuthorities);
			UserDetails user = new User(username,"$2a$10$ttZd.w89KMw5OiKxxDZ/LuMiEWZ8V3wk.p6mVa14xCHte0sLy1pNe",grantedAuthorities);
	        return user;
		}

	}

}