springsecurity讲解2

2022-11-12  本文已影响0人  念䋛

本文用示例的方式讲解,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,这样就实现了登入后,其他接口可以正常访问的流程.

上一篇下一篇

猜你喜欢

热点阅读