美文网首页
springsecurity讲解2

springsecurity讲解2

作者: 念䋛 | 来源:发表于2022-11-12 21:58 被阅读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,这样就实现了登入后,其他接口可以正常访问的流程.

    相关文章

      网友评论

          本文标题:springsecurity讲解2

          本文链接:https://www.haomeiwen.com/subject/nmzxxdtx.html