编程知识 cdmana.com

SpringSecurity入坑(五)

如何自定义实现验证?

基于SpringSecurity做基本权限验证,在之前都写的差不多了,顺便加入了在登录时,动态验证码的验证,这些都是在SpringSecuity提供好的基础上,那如何自定义这些登录的实现,仔细看一下,不管是基于内存验证jdbc验证...

都需要配置configure(AuthenticationManagerBuilder auth)身份验证管理器生成器,验证方法

/**
 * 根据传入的自定义{@link AuthenticationProvider} 认证提供者 添加身份验证。
 * 由于{@link AuthenticationProvider}实现是未知的,因此所有自定义操作必须在外部完成,
 * 并且{@link AuthenticationManagerBuilder} 会立即返回。
 */
auth.authenticationProvider(AuthenticationProvider authenticationProvider)

AuthenticationProvider是个接口,说明继承了AuthenticationProvider均可实现自定义认证的方法,SpringSecurity官方文档第9.2.1指出AuthenticationProvider,Spring Security实现的最简单的方法是DaoAuthenticationProvider所以我们实现该方法即可

MyAuthenticationProvider

package com.shaojie.authority.security;

import com.shaojie.authority.component.MyWebAuthenticationDetails;
import com.shaojie.authority.exception.VerificationCodeException;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * @author: ShaoJie
 * @data: 2020年02月10日 20:26
 * @Description: 自定义认证过程
 * <p>
 * 因为 DaoAuthenticationProvider 也是继承的 AbstractUserDetailsAuthenticationProvider
 * 所以这里就只继承 DaoAuthenticationProvider
 */
public class MyAuthenticationProvider extends DaoAuthenticationProvider {

    /**
     * 用户认证提供者
     */
    @Autowired
    private UserDetailsService userDetailsService;
    
    /**
     * 密码加密
     */
    @Autowired
    public PasswordEncoder passwordEncoder;

    public MyAuthenticationProvider(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) {
        this.setUserDetailsService(userDetailsService);
        this.setPasswordEncoder(passwordEncoder);
    }

    /**
     * 允许子类针对给定的身份验证请求对返回的(或缓存的)UserDetails 进行任何其他检查。
     * 通常,子类至少会将 Authentication#getCredentials()与 UserDetails#getPassword()比较。
     * 如果需要自定义逻辑来比较 UserDetails 和或
     * UsernamePasswordAuthenticationToken 的其他*属性,则这些属性也应出现在此方法中。
     *
     * @param userDetails    用户信息
     * @param authentication 认证方式
     */
    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails,
                                                  UsernamePasswordAuthenticationToken authentication) {
                                                  
         // getCredentials() 属于 authentication 通常用来获取主体的凭据 通常为用户的密码
        // 这里的自定义密码校验是 MyAuthenticationProvider 继承  AbstractUserDetailsAuthenticationProvider
        if (authentication.getCredentials() == null) {
            throw new BadCredentialsException(
                    this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials",
                            "密码不能为空"));
        } else {
            String password = authentication.getCredentials().toString();
            if (!password.equals(userDetails.getPassword())) {
                this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials",
                        "密码错误");
            }
        }
        // 使用父类的方法校验用户
        super.retrieveUser(userDetails.getUsername(), authentication);
    }
    
}

这里对这个UsernamePasswordAuthenticationToken应该比较疑惑,查看源码发现该类是Authentication认证方式的实现,认证方式中包含了对信息的获取

    /**
	 * The credentials that prove the principal is correct. This is usually a password,
	 * but could be anything relevant to the <code>AuthenticationManager</code>. Callers
	 * are expected to populate the credentials.
	 *
	 * @return the credentials that prove the identity of the <code>Principal</code>
	 */
	Object getCredentials();

	/**
	 * Stores additional details about the authentication request. These might be an IP
	 * address, certificate serial number etc.
	 *
	 * @return additional details about the authentication request, or <code>null</code>
	 * if not used
	 */
	Object getDetails();

简单点翻译就是Object getCredentials()方法可以获取账号的认证主体信息,通常来说就是密码这些主要的信息,Object getDetails()方法存储有关身份验证请求的其他详细信息。 但是如何能够实现对验证码的校验呢?SpringSecurity官方文档第10.16.1指出预身份验证过滤器具有一个authenticationDetailsSource属性,默认情况下,它将创建一个WebAuthenticationDetails对象来存储其他信息,那也就是说需要修改SpringSecurity实现自定义的验证配置

    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().authenticationDetailsSource(new WebAuthenticationDetailsSource());
    }

authenticationDetailsSource源码:

    /**
	 * Specifies a custom {@link AuthenticationDetailsSource}. The default is
	 * {@link WebAuthenticationDetailsSource}. 
	 * 指定一个自定义{@link AuthenticationDetailsSource} 默认为 {@link WebAuthenticationDetailsSource}. 
	 *
	 * @param authenticationDetailsSource the custom {@link AuthenticationDetailsSource}
	 * @return the {@link FormLoginConfigurer} for additional customization
	 */
	public final T authenticationDetailsSource(
			AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource) {
		this.authenticationDetailsSource = authenticationDetailsSource;
		return getSelf();
	}

authenticationDetailsSource属性需要AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource参数,但是现在需要一个WebAuthenticationDetails对象来存储其他信息

MyWebAuthenticationDetails实现校验

package com.shaojie.authority.component;

import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.web.authentication.WebAuthenticationDetails;

import javax.servlet.http.HttpServletRequest;

/**
 * @author: ShaoJie
 * @data: 2020年02月10日 21:54
 * @Description:
 */
@Slf4j
public class MyWebAuthenticationDetails extends WebAuthenticationDetails {

    /**
     * 验证码是否正确
     */
    private boolean imageCodeIsRight;

    public boolean getImageCodeIsRight() {
        return this.imageCodeIsRight;
    }

    /**
     * Records the remote address and will also set the session Id if a session already
     * exists (it won't create one). 保存会话
     * 补充用户提交的验证码和 session 保存的验证码
     *
     * @param request that the authentication request was received from
     */
    public MyWebAuthenticationDetails(HttpServletRequest request) {
        super(request);
        // 后去验证表单的值 --> 图形验证码
        String captcha = request.getParameter("captcha");
        log.info("表单的验证码captcha: {}", captcha);
        // 取出 访问时 已经添加在 session 中的验证码
        String sessionCaptcha = (String) request.getSession().getAttribute("captcha");
        log.info("session的验证码: {}", sessionCaptcha);
        // 判断两次的值是否值一样的
        if (!StrUtil.isEmpty(sessionCaptcha)) {
            // 清楚当前的验证码 无论是否成功或是失败 客户端登录失败应刷新当前的验证码
            request.getSession().removeAttribute("captcha");
            // 当验证码正确 修改当前的状态
            if (!StrUtil.isEmpty(captcha) && captcha.equals(sessionCaptcha)) {
                this.imageCodeIsRight = true;
            }
        }
    }
}

MyWebAuthenticationDetailsSource

package com.shaojie.authority.component;

import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.web.authentication.WebAuthenticationDetails;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;

/**
 * @author: ShaoJie
 * @data: 2020年02月10日 21:56
 * @Description:
 */
@Component
public class MyWebAuthenticationDetailsSource implements AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> {

    /**
     * 当类希望创建新的身份验证详细信息实例时由类调用。
     *
     * @param context 请求对象,可以由身份验证详细信息使用
     * @return
     */
    @Override
    public WebAuthenticationDetails buildDetails(HttpServletRequest context) {
        return new MyWebAuthenticationDetails(context);
    }
}

MyWebAuthenticationDetailsSourceHttpServletRequest传递给MyWebAuthenticationDetails,用于获取用户提交的信息,并验证。这时候就可以在自定义验证中加入对验证码的校验

修改后的MyAuthenticationProvider

package com.shaojie.authority.security;

import com.shaojie.authority.component.MyWebAuthenticationDetails;
import com.shaojie.authority.exception.VerificationCodeException;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * @author: ShaoJie
 * @data: 2020年02月10日 20:26
 * @Description: 自定义认证过程
 * <p>
 * 因为 DaoAuthenticationProvider 也是继承的 AbstractUserDetailsAuthenticationProvider
 * 所以这里就只继承 DaoAuthenticationProvider
 */
public class MyAuthenticationProvider extends DaoAuthenticationProvider {

    /**
     * 用户认证提供者
     */
    @Autowired
    private UserDetailsService userDetailsService;

    /**
     * 密码加密
     */
    @Autowired
    public PasswordEncoder passwordEncoder;

    public MyAuthenticationProvider(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) {
        this.setUserDetailsService(userDetailsService);
        this.setPasswordEncoder(passwordEncoder);
    }

    /**
     * 允许子类针对给定的身份验证请求对返回的(或缓存的)UserDetails 进行任何其他检查。
     * 通常,子类至少会将 Authentication#getCredentials()与 UserDetails#getPassword()比较。
     * 如果需要自定义逻辑来比较 UserDetails 和或
     * UsernamePasswordAuthenticationToken 的其他*属性,则这些属性也应出现在此方法中。
     *
     * @param userDetails    用户信息
     * @param authentication 认证方式
     * @throws AuthenticationException SneakyThrow 将避免javac坚持要求您捕获或向前抛出方法主体中语句声明它们生成的所有检查异常。
     */
    @SneakyThrows
    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails,
                                                  UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        // getCredentials() 属于 authentication 通常用来获取主体的凭据 通常为用户的密码
        // 这里的自定义密码校验是 MyAuthenticationProvider 继承  AbstractUserDetailsAuthenticationProvider
        if (authentication.getCredentials() == null) {
            throw new BadCredentialsException(
                    this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials",
                            "密码不能为空"));
        } else {
            String password = authentication.getCredentials().toString();
            if (!password.equals(userDetails.getPassword())) {
                this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials",
                        "密码错误");
            }
        }

        // 当修改了继承的类 现在实现图形验证码的 自定义
        // 实现图片验证码的逻辑
        MyWebAuthenticationDetails details = (MyWebAuthenticationDetails) authentication.getDetails();
        // 验证 验证码是正确
        if (!details.getImageCodeIsRight()) {
            throw new VerificationCodeException();
        }
    }

}

到此还没有结束,还需要修改SpringSecurity的配置,主要配置的就是authenticationDetailsSource属性,将MyWebAuthenticationDetailsSource注入进来,并调用实现自定义的认证

    @Autowired
    private AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> myWebAuthenticationDetailsSource;
    
    /**
     * 验证
     *
     * @param http
     * @throws Exception
     */
    // 代替配置文件 <security:http></security:http>
    @Override
    protected void configure(HttpSecurity http) throws Exception {

        // 添加权限
        selectPurview(http);

        http.authorizeRequests()
                // antMatchers 设置拦截的请求  hasAnyAuthority 对应的权限名称
                // .hasAnyAuthority("PRODUCT_ADD") 用户所具有的权限
                // 可替换成 .hasRole() 针对角色做验证
//                .antMatchers("/product/add").hasAnyAuthority("PRODUCT_ADD")
//                .antMatchers("/product/update").hasAnyAuthority("PRODUCT_UPDATE")
//                .antMatchers("/product/list").hasAnyAuthority("PRODUCT_LIST")
//                .antMatchers("/product/delete").hasAnyAuthority("PRODUCT_DELETE")
                // permitAll 所有的权限都能访问
                .antMatchers("/login").permitAll()
                .antMatchers("/captcha.jpg").permitAll()
//                .antMatchers("/**")
                // fullyAuthenticated 不允许匿名用户查看
//                .fullyAuthenticated()
                // 设置所有的请求都必须经过验证才能访问
                .anyRequest().authenticated()
                .and()
                // httpbasic 登录
                // .httpBasic();
                // 表单登录
                .formLogin()
                //  登录请求的页面
                .loginPage("/login")
                // 处理登录请求的 地址
                .loginProcessingUrl("/index")
                .authenticationDetailsSource(myWebAuthenticationDetailsSource)
                // 定义 故障处理器
//                 .failureHandler()
                // 修改 spring 提供的 默认登陆参数
                .usernameParameter("userName")
                .passwordParameter("password")
                .and()
                // 开启记住我功能
                .rememberMe()
                .and()
                // 开启登出
                .logout()
                // 最大会话数
                .and()
                // 添加过滤器 将 过滤器添加在 UsernamePasswordAuthenticationFilter 之前 也就是在验证账号密码之前
                // 自定义实现 用户登录拦截
//                .addFilterBefore(new VerificationCodeFilter(),
//                        UsernamePasswordAuthenticationFilter.class)
                .and()
                .csrf()
        // 禁用跨域的保护
                .disable();
    }

自定义实现登录到这里,我发现文档虽然很傻,但是好像对比源码来看的话,还是有点东西的,基本上需要的都能找到。新手不建议直接阅读,源码有的地方写的属实是看不懂,很多的地方,不对比着来看话,可能就懵了,多看点源码靠谱,最近喜欢研究研究,很多技术我有点不感冒,感觉可能也看不太多,今年目标读一本书正在循序渐进,一起加油吧,有些地方可能比较粗糙,细看吧,我这个也菜。

有些地方就不多说了,整合之前就可以了,有问题可以查看我的GitHub

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

版权声明
本文为[Shao Jie]所创,转载请带上原文链接,感谢
https://cloud.tencent.com/developer/article/1858544

Scroll to Top