基于java config的springSecurity(三)--加入RememberMe,启用CSRF和增强密码

参考http://docs.spring.io/spring-security/site/docs/3.2.5.RELEASE/reference/htmlsingle/的Remember-Me Authentication和Cross Site Request Forgery (CSRF)


一.加入remember me,下面是基于Token,还有一种基于持久化.
1.修改配置

@Override
protected void configure(HttpSecurity http) throws Exception {
	http.authorizeRequests().anyRequest().authenticated()
		.and().formLogin().loginPage("/login").failureUrl("/login?error").usernameParameter("username").passwordParameter("password").permitAll()
		.and().logout().logoutUrl("/logout").logoutSuccessUrl("/login?logout").permitAll()
		.and().rememberMe().key("9D119EE5A2B7DAF6B4DC1EF871D0AC3C");
}

2.登录页面
<input type="checkbox" name="remember-me" value="true"/>Remember me
3.测试


二.启用CSRF.一旦启用,那些Action为PATCH, POST, PUT, and DELETE的请求(包含登录和登出)都要附加CSRF Token提交到服务端.还有,登出也要使用POST(参考官方文档,当然可改为GET,但不推荐).
1.下面使用比较笨的方法:加入csrf Input标签.
a.在登录页面和其它表单都添加加入<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
b.登出:<form action="${logoutUrl}" method="post"><input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/><input type="submit" value="退出"/></form>

2.如果使用Spring MVC <form:form>标签或Thymeleaf 2.1+,使用@EnableWebMvcSecurity替换@EnableWebSecurity,那么提交的表单会自动嵌入CsrfToken提交到服务端(使用4.0.0.RC1发现@EnableWebMvcSecurity已过时,@EnableWebSecurity已有这样的功能,即用回@EnableWebSecurity即可)


三.增强密码(为密码加点盐).比较简单的方法是使用DaoAuthenticationProvider,这个AuthenticationProvider就可以设置PasswordEncoder.
1.先定义一个PasswordEncoder Bean,使用Md5PasswordEncoder不算强大,通过彩虹表破解就可能危险了,下面是使用BCryptPasswordEncoder.

@Bean
public PasswordEncoder passwordEncoder(){
	//这里的strength为4-31位,设置成16都觉得编码有点慢了
	PasswordEncoder passwordEncoder=new BCryptPasswordEncoder(4);
	return passwordEncoder;
}

关于BCryptPasswordEncoder如何加解密,就看org.springframework.security.crypto.bcrypt.BCrypt的API文档.

BCrypt implements OpenBSD-style Blowfish password hashing using the scheme described in "A Future-Adaptable Password Scheme" by Niels Provos and David Mazieres.
This password hashing system tries to thwart off-line password cracking using a computationally-intensive hashing algorithm, based on Bruce Schneier's Blowfish cipher. The work factor of the algorithm is parameterised, so it can be increased as computers get faster.
Usage is really simple. To hash a password for the first time, call the hashpw method with a random salt, like this:
String pw_hash = BCrypt.hashpw(plain_password, BCrypt.gensalt()); 
To check whether a plaintext password matches one that has been hashed previously, use the checkpw method:
if (BCrypt.checkpw(candidate_password, stored_hash))     System.out.println("It matches"); else     System.out.println("It does not match"); 
The gensalt() method takes an optional parameter (log_rounds) that determines the computational complexity of the hashing:


2.将这个PasswordEncoder和UserDetailsService共同注入到DaoAuthenticationProvider.

@Bean
public AuthenticationProvider authenticationProvider(){
	//这里使用自带的DaoAuthenticationProvider(如果满足不了需求,就参照此类再自定义)
	DaoAuthenticationProvider authenticationProvider=new DaoAuthenticationProvider();
	authenticationProvider.setUserDetailsService(userDetailsService());
	authenticationProvider.setPasswordEncoder(passwordEncoder());
	return authenticationProvider;
}

或者自己不必创建AuthenticationProvider Bean,通过重写org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter#configure(org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder)方法创建AuthenticationProvider(实际spring security内部也是创建DaoAuthenticationProvider)

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

3.可以测试登录了.可能数据库表的用户密码的数据不知怎么生成.最简单的是写一个main方法:

public static void main(String[] args) {
	PasswordEncoder passwordEncoder=new BCryptPasswordEncoder(4);
	String result=passwordEncoder.encode("123");
	System.out.println(result);
}

更改过了实体,更改一下org.exam.repository.UserRepositoryImpl#loadUserByUsername.这里是使用自已编写的JPQL来创建查询.

package org.exam.repository;
import org.exam.domain.Authority;
import org.exam.domain.User;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
 * Created by xin on 15/1/7.
 */
public class UserRepositoryImpl implements UserRepositoryCustom {
	public static final MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
	public static final String DEF_USERS_BY_USERNAME_QUERY = "SELECT u FROM User u WHERE u.username=?1";
	public static final String DEF_AUTHORITIES_BY_USERNAME_QUERY ="SELECT a FROM User u INNER JOIN u.authorities a WHERE u.username=?1";
	public static final String DEF_GROUP_AUTHORITIES_BY_USERNAME_QUERY ="SELECT a FROM User u INNER JOIN u.roles r INNER JOIN r.authorities a WHERE u.username=?1";
	@PersistenceContext
	private EntityManager entityManager;
	@Override
	@Transactional
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException{
		List<User> users=entityManager.createQuery(DEF_USERS_BY_USERNAME_QUERY, User.class).setParameter(1, username).setMaxResults(1).getResultList();
		if (users.size()==0) {
			throw new UsernameNotFoundException(messages.getMessage("UserRepositoryImpl.notFound",new Object[] {username}, "Username {0} not found"));
		}else{
			Set<Authority> authorities = new HashSet<Authority>();
			authorities.addAll(entityManager.createQuery(DEF_AUTHORITIES_BY_USERNAME_QUERY, Authority.class).setParameter(1, username).getResultList());
			authorities.addAll(entityManager.createQuery(DEF_GROUP_AUTHORITIES_BY_USERNAME_QUERY, Authority.class).setParameter(1, username).getResultList());
			Set<GrantedAuthority> grantedAuthorities=new HashSet<GrantedAuthority>(authorities.size());
			for (Authority authority:authorities){
				grantedAuthorities.add(new SimpleGrantedAuthority(authority.getAuthority()));
			}
			User user=users.get(0);
			//转为spring security用户和权限,多余信息移除,如果使用session保存认证用户,就可以减小内存占用.
			return new org.springframework.security.core.userdetails.User(user.getUsername(),user.getPassword(),user.isEnabled(),
					user.isAccountNonExpired(),user.isCredentialsNonExpired(),user.isAccountNonLocked(),grantedAuthorities);
		}
	}
}
四.其它
${pageContext.request.remoteUser}这样可以获取当前登录的用户名.jsp页面要根据权限来显示页面元素,可以先引入spring-security-taglibs包,再使用spring security标签就可以.


关于CSRF攻击:

假设银行网站提供一个表单,它允许从当前登录的用户转帐到另一个银行帐户.例如,HTTP请求可能与如下相似:
POST /transfer HTTP/1.1
Host: bank.example.com
Cookie: JSESSIONID=randomid; Domain=bank.example.com; Secure; HttpOnly
Content-Type: application/x-www-form-urlencoded
amount=100.00&routingNumber=1234&account=9876

现在再假设你认证了你的银行站点,并且没有登出,去访问了一个恶意网站.此恶意网站包含如下一个表单的HTML页面:
<form action="https://bank.example.com/transfer" method="post">
<input type="hidden" name="amount" value="100.00"/>
<input type="hidden" name="routingNumber" value="evilsRoutingNumber"/>
<input type="hidden" name="account" value="evilsAccountNumber"/>
<input type="submit" value="Win Money!"/>
</form>
你想要赢得这部分钱,那么你会点击提交按钮.在这个过程中,你无意转了100美元到一个恶意用户.这是因为,虽然恶意网站无法看到你的cookies,但是此cookies会和你关联的银行伴随着请求一起发送.
更糟的是,整个过程可以自动使用JavaScript.这意味着你甚至不需要点击按钮,那么我们如何保护自己免受这种攻击?(哈哈,当然spring security的防止csrf功能就是一种解决方案).


Spring Security提供了Remember-me功能来让用户在下次访问时无需重新登录。要启用Remember-me功能,可以按照以下步骤进行配置: 1. 在Spring Security配置文件中启用Remember-me功能,例如: ``` http .rememberMe() .key("remember-me-key") .rememberMeParameter("remember-me") .tokenValiditySeconds(86400) .userDetailsService(userDetailsService); ``` 其中,key是用来加密Remember-me cookie的密钥,rememberMeParameter是用来接收Remember-me cookie的请求参数,tokenValiditySeconds是Remember-me cookie的有效期,userDetailsService是用来根据用户名获取用户信息的服务。 2. 在登录页面中添加Remember-me的复选框,例如: ``` <input type="checkbox" name="remember-me" value="true" /> Remember me ``` 3. 在登录成功后生成Remember-me cookie,例如: ``` @RequestMapping(value = "/login", method = RequestMethod.POST) public String login(@RequestParam("username") String username, @RequestParam("password") String password, @RequestParam(value = "remember-me", required = false) boolean rememberMe, HttpServletResponse response) { // 验证用户名和密码 // ... // 生成Remember-me cookie if (rememberMe) { TokenBasedRememberMeServices rememberMeServices = new TokenBasedRememberMeServices("remember-me-key", userDetailsService); rememberMeServices.setTokenValiditySeconds(86400); rememberMeServices.setAlwaysRemember(true); rememberMeServices.loginSuccess(request, response, authentication); } // ... } ``` 其中,如果用户勾选了Remember-me复选框,则调用TokenBasedRememberMeServices的loginSuccess方法生成Remember-me cookie。 4. 在下次访问时验证Remember-me cookie,例如: ``` http .csrf().disable() .authorizeRequests() .antMatchers("/admin/**").hasRole("ADMIN") .antMatchers("/user/**").hasAnyRole("USER", "ADMIN") .anyRequest().authenticated() .and() .formLogin() .loginPage("/login") .permitAll() .and() .rememberMe() .key("remember-me-key") .rememberMeParameter("remember-me") .tokenValiditySeconds(86400) .userDetailsService(userDetailsService); ``` 其中,Remember-me cookie会在每次请求时被自动验证,如果验证通过,则用户会被认为已经登录。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值