springsecurity讲解2
本文用示例的方式讲解,springsecurity,使用session方式,
用户名密码和手机验证码两种方式
非常简陋的登入页面
image.png
该示例的代码
image.png
CustomAuthenticationFailureHandler 失败处理器
/**
认证失败处理器
**/
@Component
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException{
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("认证失败");
}
}
CustomAuthenticationSuccessHandler 成功处理器
/**
认证成功处理器
**/
@Component
public class CustomAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException{
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("认证成功");
}
}
CustomUserDetailsService 获取用户信息
/**
*
* 模拟从数据库获取用户信息,这里要注意的是,如果在配置的时候使用内存的方式,是不回使用该services
* SpringSecurityConfiguration方法中规定了使用那种方式管理用户信息,本例使用的是内存的方式
* 所以在用户名密码模式的时候,不回执行loadUserByUsername,手机登入的时候还是会走loadUserByUsername方法
*/
@Configuration
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException{
//封装用户信息,用户名和密码和权限,注意这里要注意密码应该是加密的
//省略从数据库获取详细信息
return new User(username, "1234",
AuthorityUtils.commaSeparatedStringToAuthorityList("ADMIN"));
}
}
SpringSecurityConfiguration security整体配置
@Configuration
@EnableWebSecurity
public class SpringSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
SecurityConfigurerAdapter mobileAuthenticationConfig;
@Override
protected void configure(HttpSecurity http) throws Exception{
http.csrf().disable()
.authorizeRequests()
.antMatchers("/login.html","/code/mobile").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.successHandler(new CustomAuthenticationSuccessHandler())
.failureHandler(new CustomAuthenticationFailureHandler())
.loginPage("/login")
; //浏览器以form表单形式
//将手机验证码配置放到http中,这样mobileAuthenticationConfig配置就会生效
http.apply(mobileAuthenticationConfig);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception{
// 用户信息存储在内存中
auth.inMemoryAuthentication().withUser("user")
.password(new BCryptPasswordEncoder().encode("1234")).authorities("ADMIN");
}
@Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers("/code/mobile");
}
@Bean
public PasswordEncoder passwordEncoder(){
// 官网建议的加密方式,相同的密码,每次加密都不一样,安全性更好一点
return new BCryptPasswordEncoder();
}
}
CacheValidateCode 手机验证码的内存存储
/**
* 将手机验证码保存起来,后续验证中,实际项目中要放到redis等存储
**/
public class CacheValidateCode {
public static ConcurrentHashMap<String, String> cacheValidateCodeHashMap = new ConcurrentHashMap();
}
MobileAuthenticationConfig 手机验证码配置类,在SpringSecurityConfiguration中通过http.apply方式放到springsecurity中
/**
* 用于组合其他关于手机登录的组件
*/
@Component
public class MobileAuthenticationConfig
extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired
CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;
@Autowired
CustomAuthenticationFailureHandler customAuthenticationFailureHandler;
@Autowired
UserDetailsService mobileUserDetailsService;
@Override
public void configure(HttpSecurity http) throws Exception{
MobileAuthenticationFilter mobileAuthenticationFilter = new MobileAuthenticationFilter();
// 获取容器中已经存在的AuthenticationManager对象,并传入 mobileAuthenticationFilter 里面
mobileAuthenticationFilter.setAuthenticationManager(
http.getSharedObject(AuthenticationManager.class));
// 传入 失败与成功处理器
mobileAuthenticationFilter.setAuthenticationSuccessHandler(customAuthenticationSuccessHandler);
mobileAuthenticationFilter.setAuthenticationFailureHandler(customAuthenticationFailureHandler);
// 构建一个MobileAuthenticationProvider实例,接收 mobileUserDetailsService 通过手机号查询用户信息
MobileAuthenticationProvider provider = new MobileAuthenticationProvider();
provider.setUserDetailsService(mobileUserDetailsService);
// 将provider绑定到 HttpSecurity上,并将 手机号认证过滤器绑定到用户名密码认证过滤器之后
http.authenticationProvider(provider)
.addFilterAfter(mobileAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
MobileAuthenticationFilter 手机验证filter,完全模仿UsernamePasswordAuthenticationFilter
/**
* 用于校验用户手机号是否允许通过认证
* 完全复制 UsernamePasswordAuthenticationFilter
*/
public class MobileAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private String mobileParameter = "mobile";
private String validateCodeParameter = "code";
private boolean postOnly = true;
public MobileAuthenticationFilter(){
super(new AntPathRequestMatcher("/mobile/form", "POST"));
}
// ~ Methods
// ========================================================================================================
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);
String validateCode = obtainValidateCode(request);
if(mobile == null){
mobile = "";
}
mobile = mobile.trim();
MobileAuthenticationToken authRequest = new MobileAuthenticationToken(mobile, validateCode);
// sessionID, hostname
setDetails(request, authRequest);
//认证手机码是否正确,通过provider的方式处理,使用哪个provider,是根据authRequest是哪个类型的token
//这里放的是MobileAuthenticationToken
return this.getAuthenticationManager().authenticate(authRequest);
}
/**
* 从请求中获取手机号码
*/
@Nullable
protected String obtainMobile(HttpServletRequest request){
return request.getParameter(mobileParameter);
}
/**
* 从请求中获取验证码
*/
@Nullable
protected String obtainValidateCode(HttpServletRequest request){
return request.getParameter(validateCodeParameter);
}
/**
* 将 sessionID和hostname添加 到MobileAuthenticationToken
*/
protected void setDetails(HttpServletRequest request,
MobileAuthenticationToken authRequest){
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}
/**
* 设置是否为post请求
*/
public void setPostOnly(boolean postOnly){
this.postOnly = postOnly;
}
public String getMobileParameter(){
return mobileParameter;
}
public void setMobileParameter(String mobileParameter){
this.mobileParameter = mobileParameter;
}
}
MobileAuthenticationProvider 手机验证处理器
/**
* 手机认证处理提供者,要注意supports方法和authenticate
* supports判断是否使用当前provider
* authenticate 验证手机验证码是否正确
*
*/
public class MobileAuthenticationProvider implements AuthenticationProvider {
private UserDetailsService userDetailsService;
public void setUserDetailsService(UserDetailsService userDetailsService){
this.userDetailsService = userDetailsService;
}
/**
* 认证处理:
* 1. 通过手机号码 查询用户信息( UserDetailsService实现)
* 2. 当查询到用户信息, 则认为认证通过,封装Authentication对象
*
* @param authentication
* @return
* @throws AuthenticationException
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException{
MobileAuthenticationToken mobileAuthenticationToken =
( MobileAuthenticationToken ) authentication;
// 获取手机号码
String mobile = ( String ) mobileAuthenticationToken.getPrincipal();
String validateCodeParameter = ( String ) mobileAuthenticationToken.getCredentials();
// 通过 手机号码 查询用户信息( UserDetailsService实现)
UserDetails userDetails =
userDetailsService.loadUserByUsername(mobile);
mobileAuthenticationToken.setDetails(userDetails);
// 未查询到用户信息
if(userDetails == null){
throw new AuthenticationServiceException("该手机号未注册");
}
// 1. 判断 请求是否为手机登录,且post请求
try{
// 校验验证码合法性
validate(mobile, validateCodeParameter);
}catch(AuthenticationException e){
throw new AuthenticationServiceException(e.getMessage());
}
//最终返回认证信息,这里要注意的是,返回的token中的authenticated字段要赋值为true
return createSuccessAuthentication(mobileAuthenticationToken);
}
/**
* 通过这个方法,来选择对应的Provider, 即选择MobileAuthenticationProivder
*
* @param authentication
* @return
*/
@Override
public boolean supports(Class<?> authentication){
return MobileAuthenticationToken.class.isAssignableFrom(authentication);
}
private void validate(String mobile, String inpuCode){
// 判断是否正确
if(StringUtils.isEmpty(inpuCode)){
throw new AuthenticationServiceException("验证码不能为空");
}
String cacheValidateCode = CacheValidateCode.cacheValidateCodeHashMap.get(mobile);
if(!inpuCode.equalsIgnoreCase(cacheValidateCode)){
throw new AuthenticationServiceException("验证码输入错误");
}
}
protected Authentication createSuccessAuthentication(
Authentication authentication){
// Ensure we return the original credentials the user supplied,
// so subsequent attempts are successful even with encoded passwords.
// Also ensure we return the original getDetails(), so that future
// authentication events after cache expiry contain the details
MobileAuthenticationToken result = new MobileAuthenticationToken(
authentication.getPrincipal(), authentication.getCredentials(),
AuthorityUtils.commaSeparatedStringToAuthorityList("ADMIN"));
result.setDetails(authentication.getDetails());
return result;
}
}
MobileAuthenticationToken 手机验证码的token
/**
* 创建自己的token,参考UsernamePasswordAuthenticationToken
*/
public class MobileAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
// ~ Instance fields
// ================================================================================================
private final Object principal;
private Object credentials;
// ~ Constructors
// ===================================================================================================
/**
* This constructor can be safely used by any code that wishes to create a
* <code>UsernamePasswordAuthenticationToken</code>, as the {@link #isAuthenticated()}
* will return <code>false</code>.
*/
public MobileAuthenticationToken(Object principal, Object credentials){
super(null);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(false);
}
/**
* This constructor should only be used by <code>AuthenticationManager</code> or
* <code>AuthenticationProvider</code> implementations that are satisfied with
* producing a trusted (i.e. {@link #isAuthenticated()} = <code>true</code>)
* authentication token.
*
* @param principal
* @param credentials
* @param authorities
*/
public MobileAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities){
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true); // must use super, as we override
}
// ~ Methods
// ========================================================================================================
public Object getCredentials(){
return this.credentials;
}
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();
credentials = null;
}
}
controller
@Controller
public class Congtroller {
@RequestMapping("/code/mobile")
@ResponseBody
public String mobileCode(HttpServletRequest request){
// 1. 生成一个手机验证码
String code = RandomStringUtils.randomNumeric(4);
// 2. 将手机获取的信息保存到缓存里,实际应用中,可以放到redis中
String mobile = request.getParameter("mobile");
CacheValidateCode.cacheValidateCodeHashMap.put(mobile, code);
System.out.println("手机验证码"+code);
return code;
}
}
login.html 登入页,十分简单
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录页</title>
</head>
<script src="https://upcdn.b0.upaiyun.com/libs/jquery/jquery-2.0.2.min.js"></script>
<body>
<form action="http://127.0.0.1:8080/login"method="post">
<label for="username">用户名:</label>
<input type="text" name="username" id="username">
<label for="password">密 码:</label>
<input type="password" name="password" id="password">
<button type="submit">登录</button>
</form>
<form action="http://127.0.0.1:8080/mobile/form"method="post">
<label for="mobile">手机号:</label>
<input type="text" name="mobile" id="mobile">
<label for="sendCode">验证码:</label>
<input type="text" name="code" id="sendCode">
<button type="submit">登录</button>
</form>
<button onclick="sendCode()"> 获取验证码 </button>
<script>
function sendCode() {
$.ajax(
{
type: "post",
url: "http://127.0.0.1:8080/code/mobile",
data: $("#mobile").serialize(),
success: function (result) {
alert(result);
}
}
)
}
</script>
</body>
</html>
思路非常简单,就是定义了关于手机的验证filter,并放到security中,在通过验证码登入的时候,首先创建MobileAuthenticationToken,遍历所有的provider的时候,通过support方法获取到使用哪个provider,MobileAuthenticationProvider手机验证provider,验证手机号的验证码是否正确,如果正确就将MobileAuthenticationToken放到SecurityContextHolder中,保存在ThreadLocal变量中,该线程就能使用了,并且将MobileAuthenticationToken的authenticated设置为true,在security的最后一个拦截器FilterSecurityInterceptor判断是都已经验证过了,并且判断角色是否可以访问当前接口,
这样就是验证的整个流程,session的方式验证,在登入成功的时候token放到tomcat的内存中了,key就是sessionid,前端将session传到server时,从tomcat中获取已经验证过的token,这样就实现了登入后,其他接口可以正常访问的流程.