精华内容
下载资源
问答
  • Spring Security用户认证和权限控制(自定义实现)

    万次阅读 多人点赞 2019-03-29 10:04:48
    Spring Security用户认证和权限控制(自定义实现)1 说明2 用户认证相关的自定义实现2.1 自定义用户认证页面2.2 自定义退出功能2.3 自定义用户认证拦截器2.4 自定义用户认证处理器2.5 自定义用户认证对象2.6 自定义...

    1 说明

    Spring Security用户认证和权限控制(默认实现)这篇文章介绍了Spring Security默认实现的用户认证和权限控制这两大功能的使用方法和原理分析,但是,Spring Security框架也能够支持自定义的用户认证和权限控制的实现逻辑。

    本文主要是在Spring Security用户认证和权限控制(默认实现)这篇文章的基础上介绍自定义的用户认证和权限控制的实现方法,以帮助读者实现满足个性化需求的用户认证和权限控制的功能。

    2 用户认证相关的自定义实现

    2.1 自定义用户认证页面

    Spring Security默认的用户认证页面不太美观,也只能且必须输入用户名和密码,因此,一般都会选择自定义用户认证页面。自定义用户认证页面只需要以下几步:

    1. 在Spring Security配置类中通过http.formlogin()设置表单登录时,通过loginPage(" URL")来指定请求认证页面的URL,以及通过loginProcessingUrl(" URL")来指定自定义的用户认证页面提交认证表单后发送请求的URL。当然,还可以通过defaultSuccessUrl(" URL")来指定默认认证成功后请求的URL。如下图所示:
      在这里插入图片描述
    2. 在Controller中写一个Get类型的方法,该方法的请求URL要与第1步中loginPage(" URL")配置的URL保持一致(本文中都为/login),且请求该方法后跳转到自定义的用户认证页面,如下图所示:
      在这里插入图片描述
    3. 创建一个自定义的用户认证页面,页面的文件名称要与第2步中定义的方法的返回值保持一致(本文中都为login.html),且页面中表单提交的URL要与第1步中loginProcessingUrl(" URL")配置的URL保持一致(本文中都为/login),表单中可以根据个性化需求设定用户认证所需要的认证信息,如下图所示:
      在这里插入图片描述
      注意:如果想要在自定义的用户认证页面中使用"记住我"功能,则需要在自定义的用户认证页面中设置一个参数名为remember-me的选择框,并设置值为true,如上图中所示。

    2.2 自定义退出功能

    Spring Security提供了默认的用户认证和退出功能,但是当使用了自定义的用户认证页面之后,默认的退出功能就会无法使用。这是因为使用了自定义的用户认证页面后,Spring Security框架就不会创建DefaultLogoutPageGeneratingFilter对象(该对象主要是拦截GET类型的/logout请求并跳转到退出确认页面,用户在退出确认页面点击了确认按钮后才发送POST类型的/logout请求),通过在浏览器地址栏中请求/logout是GET类型的请求,因此,请求无法被处理,从而报错。而真正的退出需要POST类型的/logout请求。关于自定义用户认证页面导致退出功能异常的原因与解决方案具体请见Spring Security使用自定义的用户登录页面导致退出功能异常的原因分析与解决方案这篇文章。

    我们可以在Spring Security配置类中通过http.logout()设置退出功能,并通过logoutRequestMatcher(RequestMatcher)来指定对GET类型和POST类型的/logout请求都执行退出操作,如下图所示:
    在这里插入图片描述
    在这里插入图片描述
    同时,也可以通过logoutUrl(" URL")来指定执行退出操作的URL(本文中都为/logout),以及通过logoutSuccessUrl(" URL")指定退出成功后自动请求的URL。

    2.3 自定义记住我功能

    Spring Security默认实现了"记住我"功能,具体而言是:用户认证时如果选择了"记住我"功能,则认证成功之后,Spring Security框架会通过UUID随机生成一个能够代表该用户已认证的Token,然后把该Token保存到内存中,同时保存到用户浏览器的Cookie中;当用户在Token的有效期内,关闭了浏览器之后再次发送访问请求时,就会通过Cookie中的Token自动通过认证,即不需要用户再输入账号和密码进行认证。

    Spring Security默认是将代表用户已经认证的Token保存在内存中,但是也能够支持把Token保存到数据库中,我们可以在Spring Security配置类中通过http.rememberMe()设置记住我功能,并通过tokenRepository(PersistentTokenRepository)来指定保存Token的位置,如下图所示:
    在这里插入图片描述
    因此,需要我们自己创建一个PersistentTokenRepository对象,并指定数据源,如下图所示:
    在这里插入图片描述
    在这里插入图片描述

    2.4 自定义用户认证拦截器

    用户认证拦截器主要是拦截用户发送的认证请求,然后从请求中获取用户账号和密码等认证信息并封装成一个未认证的AthenticationToken对象,然后调用AuthenticationManager对AthenticationToken进行认证。

    自定义的用户认证拦截器,需要继承AbstractAuthenticationProcessingFilter,并重写public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)方法,本文自定义的用户认证拦截器的完整代码如下所示:

    package com.custom.authentication.server.authentication;
    
    import com.custom.authentication.server.constant.ConfigConstant;
    import org.springframework.security.authentication.AuthenticationServiceException;
    import org.springframework.security.authentication.InternalAuthenticationServiceException;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
    import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
    import org.springframework.util.Assert;
    import org.springframework.util.StringUtils;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    /**
     * 自定义的用户名密码认证过滤器
     */
    public class CustomUsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    
        private String usernameParameter = "username";
        private String passwordParameter = "password";
        private boolean postOnly = true;
    
        public CustomUsernamePasswordAuthenticationFilter() {
            /**
             * 设置该过滤器对POST请求/login进行拦截
             */
            super(new AntPathRequestMatcher(ConfigConstant.LOGIN_FORM_SUBMIT_URL, "POST"));
        }
    
        @Override
        public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
            if (this.postOnly && !request.getMethod().equals("POST")) {
                throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
            } else {
                /**
                 * 从http请求中获取用户输入的用户名和密码信息
                 * 这里接收的是form形式的参数,如果要接收json形式的参数,修改这里即可
                 */
                String username = this.obtainUsername(request);
                String password = this.obtainPassword(request);
                if(StringUtils.isEmpty(username) && StringUtils.isEmpty(password)) {
                    throw new UsernameNotFoundException("CustomUsernamePasswordAuthenticationFilter获取用户认证信息失败");
                }
                /**
                 * 使用用户输入的用户名和密码信息创建一个未认证的用户认证Token
                 */
                CustomUsernamePasswordAuthenticationToken authRequest = new CustomUsernamePasswordAuthenticationToken(username, password);
                /**
                 * 设置一些详情信息
                 */
                this.setDetails(request, authRequest);
                /**
                 * 通过AuthenticationManager调用相应的AuthenticationProvider进行用户认证
                 */
                return this.getAuthenticationManager().authenticate(authRequest);
            }
        }
    
        protected String obtainUsername(HttpServletRequest request) {
            return request.getParameter(this.usernameParameter);
        }
    
        protected String obtainPassword(HttpServletRequest request) {
            return request.getParameter(this.passwordParameter);
        }
    
        protected void setDetails(HttpServletRequest request, CustomUsernamePasswordAuthenticationToken authRequest) {
            authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
        }
    
        public void setUsernameParameter(String usernameParameter) {
            Assert.hasText(usernameParameter, "Username parameter must not be empty or null");
            this.usernameParameter = usernameParameter;
        }
    
        public void setPasswordParameter(String passwordParameter) {
            Assert.hasText(passwordParameter, "Password parameter must not be empty or null");
            this.passwordParameter = passwordParameter;
        }
    
        public void setPostOnly(boolean postOnly) {
            this.postOnly = postOnly;
        }
    
        public final String getUsernameParameter() {
            return this.usernameParameter;
        }
    
        public final String getPasswordParameter() {
            return this.passwordParameter;
        }
    }
    

    自定义的用户认证拦截器要在构造方法中指定拦截的认证请求(本文中是POST类型的/login请求,可根据需求设置),并在attemptAuthentication()方法中实现获取用户认证信息、封装AuthenticationToken对象、调用AuthenticationManager对AuthenticationToken进行认证等逻辑。

    注意:自定义的用户认证拦截器的使用请见2.9小节。

    2.5 自定义用户认证处理器

    用户认证处理器主要是对用户提交的认证信息进行认证,Spring Security默认实现的认证处理器的认证处理逻辑并不一定符合所有的业务需求(例如,默认的认证处理无法处理验证码),因此,可以自定义用户认证处理器。

    自定义的用户认证处理器,需要实现AuthenticationProvider接口,主要是实现public Authentication authenticate(Authentication authentication)方法和public boolean supports(Class<?> authentication)方法,前者主要是实现具体的认证逻辑,后者主要是指定认证处理器能对哪种AuthenticationToken对象进行认证。本文自定义的用户认证处理器的完整代码如下所示:

    package com.custom.authentication.server.authentication;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.authentication.AuthenticationProvider;
    import org.springframework.security.authentication.InternalAuthenticationServiceException;
    import org.springframework.security.core.Authentication;
    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.password.PasswordEncoder;
    import org.springframework.stereotype.Component;
    
    /**
     * 自定义的用户名密码认证实现类
     */
    @Component("customUsernamePasswordAuthenticationProvider")
    public class CustomUsernamePasswordAuthenticationProvider implements AuthenticationProvider {
    
        @Autowired
        private PasswordEncoder passwordEncoder;
    
        @Autowired
        private UserDetailsService userDetailsServiceImpl;
    
        @Override
        public Authentication authenticate(Authentication authentication) throws AuthenticationException {
            /**
             * 将未认证的Authentication转换成自定义的用户认证Token
             */
            CustomUsernamePasswordAuthenticationToken authenticationToken = (CustomUsernamePasswordAuthenticationToken) authentication;
    
            /**
             * 根据用户Token中的用户名查找用户信息,如果有该用户信息,则验证用户密码是否正确
             */
            UserDetails user = userDetailsServiceImpl.loadUserByUsername((String)(authenticationToken.getPrincipal()));
            if(user == null) {
                throw new InternalAuthenticationServiceException("CustomUsernamePasswordAuthenticationProvider获取认证用户信息失败");
            } else if(!this.passwordEncoder.matches((CharSequence) authenticationToken.getCredentials(), user.getPassword())) {
                throw new BadCredentialsException("用户名或密码不正确");
            }
            /**
             * 认证成功则创建一个已认证的用户认证Token
             */
            CustomUsernamePasswordAuthenticationToken authenticationResult = new CustomUsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities());
            /**
             * 设置一些详情信息
             */
            authenticationResult.setDetails(authenticationToken.getDetails());
            return authenticationResult;
        }
    
        @Override
        public boolean supports(Class<?> authentication) {
            /**
             * 指定该认证处理器能对CustomUsernamePasswordAuthenticationToken对象进行认证
             */
            return CustomUsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
        }
    
    }
    

    注意:自定义的用户认证处理器的使用请见2.9小节。

    2.6 自定义用户认证对象

    用户认证对象是在用户认证拦截器中创建的,在用户认证处理器中使用的,具体可见2.4节和2.5节中。

    用户认证对象(AuthenticationToken)中封装的是用户认证信息,例如,UsernamePasswordAuthenticationToken中封装的是用户名和密码。实际业务中,可能需要根据不同的用户信息进行认证(例如,手机号和验证码),此时就需要自定义用户认证对象。

    自定义的用户认证对象,需要继承AbstractAuthenticationToken类,并设定根据认证时使用的是哪些信息。本文自定义的用户认证对象的完整代码如下所示:

    package com.custom.authentication.server.authentication;
    
    import org.springframework.security.authentication.AbstractAuthenticationToken;
    import org.springframework.security.core.GrantedAuthority;
    import java.util.Collection;
    
    /**
     * 自定义的用户名密码认证对象
     */
    public class CustomUsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
        /**
         * 用户名
         */
        private final Object principal;
    
        /**
         * 密码
         */
        private Object credentials;
    
        /**
         * 创建未认证的用户名密码认证对象
         */
        public CustomUsernamePasswordAuthenticationToken(Object principal, Object credentials) {
            super((Collection)null);
            this.principal = principal;
            this.credentials = credentials;
            this.setAuthenticated(false);
        }
    
        /**
         * 创建已认证的用户密码认证对象
         */
        public CustomUsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
            super(authorities);
            this.principal = principal;
            this.credentials = credentials;
            super.setAuthenticated(true);
        }
    
        @Override
        public Object getCredentials() {
            return this.credentials;
        }
    
        @Override
        public Object getPrincipal() {
            return this.principal;
        }
    
        @Override
        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");
            } else {
                super.setAuthenticated(false);
            }
        }
    
        @Override
        public void eraseCredentials() {
            super.eraseCredentials();
            this.credentials = null;
        }
    
    }
    

    注意:自定义的用户认证对象的使用请见2.9小节。

    2.7 自定义用户认证成功处理器

    用户认证成功处理器是在用户认证成功之后调用,主要是执行一些额外的操作(例如,操作Cookie、页面跳转等)。

    自定义的用户认证成功处理器可以通过实现AuthenticationSuccessHandler接口,或者通过继承AbstractAuthenticationTargetUrlRequestHandler类及其子类来实现。本文自定义的用户认证成功处理器是通过继承AbstractAuthenticationTargetUrlRequestHandler的子类SavedRequestAwareAuthenticationSuccessHandler来实现的,完整代码如下所示:

    package com.custom.authentication.server.authentication;
    
    import com.custom.authentication.server.constant.ConfigConstant;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
    import org.springframework.stereotype.Component;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    
    /**
     * 自定义的用户认证成功处理器
     */
    @Component("customAuthenticationSuccessHandler")
    public class CustomAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
    
        @Autowired
        private ObjectMapper objectMapper;
    
        public CustomAuthenticationSuccessHandler() {
            /**
             * 指定默认登录成功请求的URL和是否总是使用默认登录成功请求的URL
             * 注意:自定义的认证成功处理器,如果不指定,默认登录成功请求的URL是"/"
             */
            this.setDefaultTargetUrl(ConfigConstant.DEFAULT_LOGIN_SUCCESSFUL_REQUEST_URL);
            this.setAlwaysUseDefaultTargetUrl(ConfigConstant.ALWAYS_USE_DEFAULT_LOGIN_SUCCESSFUL_REQUEST_URL);
        }
    
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
            /**
             * 如果配置ConfigConstant.LOGIN_RESPONSE_TYPE="JSON",则返回JSON,否则使用页面跳转
             */
            if("JSON".equalsIgnoreCase(ConfigConstant.LOGIN_RESPONSE_TYPE)) {
                response.setContentType("application/json;charset=UTF-8");
                response.getWriter().write(objectMapper.writeValueAsString(authentication));
            } else {
                super.onAuthenticationSuccess(request, response, authentication);
            }
        }
    
    }
    

    需要说明的是,如果自定义了用户认证成功处理器,则在Spring Security配置类中通过http.formlogin().defaultSuccessUrl(" URL")来指定的默认认证成功后请求的URL配置并不会对自定义的用户认证成功处理器有效,因为该配置只对Spring Security默认的用户认证成功处理器有效。要想使得自定义的用户认证成功之后自动请求指定的URL,则需要在自定义的用户认证成功处理器中通过设置defaultTargetUrl参数的值的方式来实现,如下图所示:
    在这里插入图片描述
    注意:自定义的用户认证成功处理器的使用请见2.9小节。

    2.8 自定义用户认证失败处理器

    用户认证失败处理器是在用户认证失败之后调用,主要是执行一些额外的操作(例如,操作Cookie、页面跳转等)。

    自定义的用户认证失败处理器可以通过实现AuthenticationFailureHandler接口,或者通过继承AuthenticationFailureHandler接口的其它实现类来实现。本文自定义的用户认证失败处理器是通过继承AuthenticationFailureHandler接口的实现类SimpleUrlAuthenticationFailureHandler来实现的,完整代码如下所示:

    package com.custom.authentication.server.authentication;
    
    import com.custom.authentication.server.constant.ConfigConstant;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.http.HttpStatus;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
    import org.springframework.stereotype.Component;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    
    /**
     * 自定义的用户认证失败处理器
     */
    @Component("customAuthenticationFailureHandler")
    public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
    
        @Autowired
        private ObjectMapper objectMapper;
    
        @Override
        public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
            /**
             * 如果配置ConfigConstant.LOGIN_RESPONSE_TYPE="JSON",则返回JSON,否则使用页面跳转
             */
            if("JSON".equalsIgnoreCase(ConfigConstant.LOGIN_RESPONSE_TYPE)) {
                response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
                response.setContentType("application/json;charset=UTF-8");
                response.getWriter().write(objectMapper.writeValueAsString(exception.getMessage()));
            } else {
                super.onAuthenticationFailure(request, response, exception);
            }
        }
    
    }
    

    注意:自定义的用户认证失败处理器的使用请见2.9小节。

    2.9 自定义用户认证处理逻辑的应用

    自定义的用户认证页面、退出功能、"记住我"功能的使用方法比较简单,分别按照2.1节、2.2节、2.3节中的实现即可。

    自定义的用户认证拦截器、用户认证处理器、用户认证对象、认证成功处理器、认证失败处理器的使用方法却要稍微复杂,仅仅只是自定义实现了这些部分还不够,还要使这些自定义的实现生效才行。具体做法是:

    1. 通过继承SecurityConfigurerAdapter类实现一个配置类,并重写public void configure(HttpSecurity http)方法,在该方法中对自定义的用户认证拦截器进行设置,然后再将自定义的用户认证拦截器和处理器加入到过滤器链中特定的位置。
    2. 然后在Spring Security配置类中应用第1步实现的配置类。

    本文实现的配置类的完整代码如下所示:

    package com.custom.authentication.server.config;
    
    import com.custom.authentication.server.authentication.CustomAuthenticationFailureHandler;
    import com.custom.authentication.server.authentication.CustomAuthenticationSuccessHandler;
    import com.custom.authentication.server.authentication.CustomUsernamePasswordAuthenticationFilter;
    import com.custom.authentication.server.authentication.CustomUsernamePasswordAuthenticationProvider;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.web.DefaultSecurityFilterChain;
    import org.springframework.security.web.authentication.RememberMeServices;
    import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
    import org.springframework.stereotype.Component;
    
    /**
     * 自定义的用户名密码认证配置类
     */
    @Component
    public class CustomUsernamePasswordAuthenticationConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    
        @Autowired
        CustomUsernamePasswordAuthenticationProvider customUsernamePasswordAuthenticationProvider;
    
        @Autowired
        CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;
    
        @Autowired
        CustomAuthenticationFailureHandler customAuthenticationFailureHandler;
    
        @Override
        public void configure(HttpSecurity http) throws Exception {
            CustomUsernamePasswordAuthenticationFilter authenticationFilter = new CustomUsernamePasswordAuthenticationFilter();
    
            /**
             * 自定义用户认证处理逻辑时,需要指定AuthenticationManager,否则无法认证
             */
            authenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
            /**
             * 自定义用户认证处理逻辑时,需要指定RememberMeServices,否则自定义用户认证的"记住我"功能异常
             */
            authenticationFilter.setRememberMeServices(http.getSharedObject(RememberMeServices.class));
    
            /**
             * 指定自定义的认证成功和失败的处理器
             */
            authenticationFilter.setAuthenticationSuccessHandler(customAuthenticationSuccessHandler);
            authenticationFilter.setAuthenticationFailureHandler(customAuthenticationFailureHandler);
    
            /**
             * 把自定义的用户名密码认证过滤器和处理器添加到UsernamePasswordAuthenticationFilter过滤器之前
             */
            http
                .authenticationProvider(customUsernamePasswordAuthenticationProvider)
                .addFilterBefore(authenticationFilter, UsernamePasswordAuthenticationFilter.class);
        }
    
    }
    

    注意:必须通过setAuthenticationManager()方法给自定义的用户认证拦截器注入一个AuthenticationManager对象才能调用相应的用户认证处理器进行认证。必须通过setRememberMeServices()方法给自定义的用户认证拦截器注入一个RememberMeServices对象才能在自定义的用户认证中使用"记住我"功能。

    本文的Spring Security配置类的完整代码如下所示:

    package com.custom.authentication.server.config;
    
    import com.custom.authentication.server.constant.ConfigConstant;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.crypto.password.PasswordEncoder;
    import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
    import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
    import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
    import org.springframework.security.web.util.matcher.OrRequestMatcher;
    import javax.sql.DataSource;
    
    /**
     * Spring Security配置类
     */
    @EnableWebSecurity
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Autowired
        private DataSource dataSource;
    
        @Autowired
        private UserDetailsService userDetailsServiceImpl;
    
        @Autowired
        private CustomUsernamePasswordAuthenticationConfig customUsernamePasswordAuthenticationConfig;
    
        /**
         * 用户认证配置
         */
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            /**
             * 指定用户认证时,默认从哪里获取认证用户信息
             */
            auth.userDetailsService(userDetailsServiceImpl);
        }
    
        /**
         * Http安全配置
         */
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                .csrf()
                    .disable()
                .apply(customUsernamePasswordAuthenticationConfig)
                    .and()
                .formLogin()
                    .loginPage(ConfigConstant.REQUEST_LOGIN_PAGE_URL)
                    .loginProcessingUrl(ConfigConstant.LOGIN_FORM_SUBMIT_URL)
                    .defaultSuccessUrl(ConfigConstant.DEFAULT_LOGIN_SUCCESSFUL_REQUEST_URL, ConfigConstant.ALWAYS_USE_DEFAULT_LOGIN_SUCCESSFUL_REQUEST_URL)
                    .permitAll()
                    .and()
                .logout()
                    .logoutUrl(ConfigConstant.LOGOUT_URL)
                    .logoutSuccessUrl(ConfigConstant.LOGOUT_SUCCESSFUL_REQUEST_URL)
                    .logoutRequestMatcher(getLogoutRequestMatchers())
                    .permitAll()
                    .and()
                .rememberMe()
                    .tokenRepository(getPersistentTokenRepository())
                    .tokenValiditySeconds(ConfigConstant.REMEMBER_ME_SECOND)
                    .and()
                .authorizeRequests()
                    .antMatchers(ConfigConstant.PERMIT_ALL_REQUEST_URL_ARRAY)
                    .permitAll()
                    .anyRequest()
                    .authenticated();
        }
    
        /**
         * 密码加密器
         */
        @Bean
        public PasswordEncoder passwordEncoder() {
            /**
             * BCryptPasswordEncoder:相同的密码明文每次生成的密文都不同,安全性更高
             */
            return new BCryptPasswordEncoder();
        }
    
        /**
         * 指定保存用户登录“记住我”功能的Token的方法:
         * 默认是使用InMemoryTokenRepositoryImpl将Token放在内存中,
         * 如果使用JdbcTokenRepositoryImpl,会创建persistent_logins数据库表,并将Token保存到该表中。
         */
        @Bean
        public PersistentTokenRepository getPersistentTokenRepository() {
            JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
            jdbcTokenRepository.setDataSource(dataSource);
            /**
             * 系统启动时自动创建表,只需要在第一次启动系统时创建即可,因此这行代码只在需要创建表时才启用
             */
    //        jdbcTokenRepository.setCreateTableOnStartup(true);
            return jdbcTokenRepository;
        }
    
        /**
         * 自定义退出登录的RequestMatcher
         */
        private OrRequestMatcher getLogoutRequestMatchers() {
            /**
             * 用户退出登录时,匹配GET请求/logout和POST请求/logout,使得这两种请求都执行退出登录操作
             * 默认情况(未禁用跨域请求伪造,且自定义用户登录页面)下,只对POST请求/logout才执行退出登录操作
             */
            AntPathRequestMatcher getLogoutRequestMatcher = new AntPathRequestMatcher(ConfigConstant.LOGOUT_URL, "GET");
            AntPathRequestMatcher postLogoutRequestMatcher = new AntPathRequestMatcher(ConfigConstant.LOGOUT_URL, "POST");
            return new OrRequestMatcher(getLogoutRequestMatcher, postLogoutRequestMatcher);
        }
    
    }
    

    其中应用第1步实现的配置类的代码如下图中所示:
    在这里插入图片描述

    3 权限控制相关的自定义实现

    Spring Security用户认证和权限控制(默认实现)这篇文章中介绍了方法级别的权限控制,而本文要介绍的是URL级别的权限控制。

    3.1 自定义权限数据获取类

    Spring Security的方法级别的权限控制的默认实现是把权限信息保存在内存中,而基于URL级别的权限控制的实际应用中通常是把权限信息保存在数据库中。因此,URL级别的权限控制通常都需要我们自己实现从数据库中获取权限信息。

    自定义的权限数据获取类可以通过实现FilterInvocationSecurityMetadataSource 接口来实现。本文自定义的权限数据获取类的完整代码如下所示:

    package com.custom.authentication.server.permission;
    
    import org.springframework.security.access.ConfigAttribute;
    import org.springframework.security.access.SecurityConfig;
    import org.springframework.security.web.FilterInvocation;
    import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
    import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
    import org.springframework.stereotype.Component;
    import javax.servlet.http.HttpServletRequest;
    import java.util.ArrayList;
    import java.util.Collection;
    import java.util.HashMap;
    import java.util.Iterator;
    import java.util.List;
    
    /**
     * 自定义的资源(url)权限(角色)数据获取类
     */
    @Component("customFilterInvocationSecurityMetadataSource")
    public class CustomFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
    
        /**
         * 每个资源(url)所需要的权限(角色)集合
         */
        private static HashMap<String, Collection<ConfigAttribute>> map =null;
    
        /**
         * 获取每个资源(url)所需要的权限(角色)集合
         * 这里应该从数据库中动态查询,这里为了方便而直接创建
         */
        private void getResourcePermission(){
            map = new HashMap<>();
    
            /**
             * 创建两个权限:ROLE_ADMIN 和 ROLE_EMPLOYEE
             */
            ConfigAttribute adminRole = new SecurityConfig("ROLE_ADMIN");
            ConfigAttribute employeeRole = new SecurityConfig("ROLE_EMPLOYEE");
    
            /**
             * 创建两个权限集合
             */
            List<ConfigAttribute> adminUrlRoles = new ArrayList<>();
            adminUrlRoles.add(adminRole);
            List<ConfigAttribute> employeeUrlRoles = new ArrayList<>();
            employeeUrlRoles.add(employeeRole);
    
            /**
             * 设置资源(url)所需要的权限(角色)集合
             */
            map.put("/toAdmin", adminUrlRoles);
            map.put("/toEmployee", employeeUrlRoles);
            map.put("/toUser", null);
            map.put("/toAbout", null);
            map.put("/toHome", null);
            map.put("/getPrincipal", null);
            map.put("/getUserDetails", null);
        }
    
        /**
         * 获取用户请求的某个具体的资源(url)所需要的权限(角色)集合
         * @param object 包含了用户请求的request信息
         */
        @Override
        public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
            if(map ==null) {
                getResourcePermission();
            }
            HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
    
            /**
             * 遍历每个资源(url),如果与用户请求的资源(url)匹配,则返回该资源(url)所需要的权限(角色)集合,
             * 如果全都不匹配,则表示用户请求的资源(url)不需要权限(角色)即可访问
             */
            Iterator<String> iter = map.keySet().iterator();
            while(iter.hasNext()) {
                String url = iter.next();
                if(new AntPathRequestMatcher(url).matches(request)) {
                    return map.get(url);
                }
            }
            return null;
        }
    
        @Override
        public Collection<ConfigAttribute> getAllConfigAttributes() {
            return null;
        }
    
        @Override
        public boolean supports(Class<?> clazz) {
            return true;
        }
    }
    

    注意:自定义的权限数据获取类的使用请见3.3小节。

    3.2 自定义权限控制管理器

    权限控制管理器主要是比对用户所拥有的权限和用户请求的URL或方法所需要的权限,以决定是否允许用户执行请求,如果比对成功,则允许,否则拒绝。

    自定义的权限控制管理器主要是要实现权限比对的处理逻辑,可以通过实现AccessDecisionManager接口来实现。本文自定义的权限控制管理器的完整代码如下所示:

    package com.custom.authentication.server.permission;
    
    import org.springframework.security.access.AccessDecisionManager;
    import org.springframework.security.access.AccessDeniedException;
    import org.springframework.security.access.ConfigAttribute;
    import org.springframework.security.authentication.InsufficientAuthenticationException;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.stereotype.Component;
    import java.util.Collection;
    import java.util.Iterator;
    
    /**
     * 自定义的权限控制管理器
     */
    @Component("customAccessDecisionManager")
    public class CustomAccessDecisionManager implements AccessDecisionManager {
    
        /**
         * 判断是否有权限
         * @param auth  包含了UserDetails用户信息
         * @param object  包含了request请求信息
         * @param configAttributes  由CustomFilterInvocationSecurityMetadataSource.getAttributes(object)返回的请求的资源(url)所需要的权限(角色)集合
         */
        @Override
        public void decide(Authentication auth, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
            /**
             * 如果请求的资源不需要权限,则直接放行
             */
            if(configAttributes == null || configAttributes.size() <= 0) {
                return;
            }
            /**
             * 判断用户所拥有的权限是否是资源所需要的权限之一,如果是则放行,否则拦截
             */
            Iterator<ConfigAttribute> iter = configAttributes.iterator();
            while(iter.hasNext()) {
                String needRole = iter.next().getAttribute();
                for(GrantedAuthority grantRole : auth.getAuthorities()) {
                    if(needRole.trim().equals(grantRole.getAuthority().trim())) {
                        return;
                    }
                }
            }
            throw new AccessDeniedException("no privilege");
        }
    
        @Override
        public boolean supports(ConfigAttribute attribute) {
            return true;
        }
    
        @Override
        public boolean supports(Class<?> clazz) {
            return true;
        }
    }
    

    注意:自定义的权限控制管理器的使用请见3.3小节。

    3.3 自定义权限控制处理逻辑的应用

    启用方法级别的权限控制,需要在主启动类上使用@EnableGlobalMethodSecurity()注解,并在请求方法的入口处使用类似于@PreAuthorize(“hasRole(‘ROLE_ADMIN’)”)的注解指定方法执行所需要的权限。

    自定义的URL级别的权限控制的启用方法与方法级别的权限控制的启用方法不太一样,而是通过把自定义的权限数据获取类和权限控制管理器添加到自定义的过滤器中实现的,具体做法是:

    1. 自定义一个既继承了AbstractSecurityInterceptor类又实现了Filter接口的过滤器,并重写和实现其中的方法。
    2. 把自定义的权限数据获取类的对象注入到自定义的过滤器中,并且要能够通过调用重写的public SecurityMetadataSource obtainSecurityMetadataSource()方法即可获取到自定义的权限数据获取类的对象。
    3. 以构造方法的形式把自定义的权限控制管理器注入到自定义的过滤器中。

    本文实现的自定义过滤器的完整代码如下所示:

    package com.custom.authentication.server.permission;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.access.SecurityMetadataSource;
    import org.springframework.security.access.intercept.AbstractSecurityInterceptor;
    import org.springframework.security.access.intercept.InterceptorStatusToken;
    import org.springframework.security.web.FilterInvocation;
    import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
    import org.springframework.stereotype.Component;
    import javax.servlet.Filter;
    import javax.servlet.FilterChain;
    import javax.servlet.FilterConfig;
    import javax.servlet.ServletException;
    import javax.servlet.ServletRequest;
    import javax.servlet.ServletResponse;
    import java.io.IOException;
    
    /**
     * 自定义的权限验证过滤器
     */
    @Component
    public class CustomFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
    
        /**
         * 注入自定义的资源(url)权限(角色)获取类
         */
        @Autowired
        private FilterInvocationSecurityMetadataSource customFilterInvocationSecurityMetadataSource;
    
        /**
         * 注入自定义的权限验证管理器
         */
        @Autowired
        public void setAccessDecisionManager(CustomAccessDecisionManager customAccessDecisionManager) {
            super.setAccessDecisionManager(customAccessDecisionManager);
        }
    
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {}
    
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
            FilterInvocation fi = new FilterInvocation(request, response, chain);
            InterceptorStatusToken token = super.beforeInvocation(fi);
            try {
                /**
                 * 执行下一个拦截器
                 */
                fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
            } finally {
                super.afterInvocation(token, null);
            }
        }
    
        @Override
        public void destroy() {}
    
        @Override
        public Class<?> getSecureObjectClass() {
            return FilterInvocation.class;
        }
    
        @Override
        public SecurityMetadataSource obtainSecurityMetadataSource() {
            return this.customFilterInvocationSecurityMetadataSource;
        }
    }
    

    3 总结

    本文对于使用Spring Security实现用户认证和权限控制功能当中可能需要个性化的部分进行了介绍,同时也对自定义实现中需要注意的一些小细节进行了特别说明。关于自定义实现的测试方法和Spring Security用户认证和权限控制(默认实现)这篇文章中的一样,因此就不过多重复。关于授权服务器、资源服务器的内容可以查阅以下几篇文章:

    OAuth2授权服务器和四种授权方式 这篇文章介绍了授权服务器和四种授权方式的配置与使用方法。
    OAuth2资源服务器 这篇文章介绍了基于方法级别的权限控制的资源服务器的配置与使用方法。

    如果觉得本文对您有帮助,请关注博主的微信公众号,会经常分享一些Java和大数据方面的技术案例!
    在这里插入图片描述

    展开全文
  • Spring Security用户认证和权限控制(默认实现)

    万次阅读 多人点赞 2019-03-28 13:58:02
    Spring Security用户认证和权限控制(默认实现) 1 背景 2 实战示例 2.1 创建工程 2.2 配置说明 2.2 用户认证 2.3 权限控制 3 功能测试 3.1 用户认证功能测试 3.2 "记住我" 功能测试 3.3 退出功能测试 3.4 权限控制...

    1 背景

    实际应用系统中,为了安全起见,一般都必备用户认证(登录)和权限控制的功能,以识别用户是否合法,以及根据权限来控制用户是否能够执行某项操作。

    Spring Security是一个安全相关的框架,能够与Spring项目无缝整合,本文主要是介绍Spring Security默认的用户认证和权限控制的使用方法和原理,但不涉及到自定义实现。

    Spring Security用户认证和权限控制(自定义实现)这篇文章专门讲解用户认证和权限控制相关的自定义实现。

    2 实战示例

    2.1 创建工程

    创建一个名为authentication-server的spring boot工程,项目结构如下图所示:
    在这里插入图片描述
    说明:该spring boot工程主要是整合了Spring Security框架和Spring MVC框架。

    2.2 配置说明

    pom.xml配置文件如下所示:

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>com.authentication.server</groupId>
        <artifactId>authentication-server</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <name>authentication-server</name>
        <description>统一用户认证中心</description>
    
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.1.3.RELEASE</version>
            <relativePath/> <!-- lookup parent from repository -->
        </parent>
    
        <properties>
            <java.version>1.8</java.version>
        </properties>
    
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-security</artifactId>
            </dependency>
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
    
    </project>
    

    application.yml配置文件如下所示:

    server:
      port: 8080
    

    2.2 用户认证

    要想使用Spring Security框架,配置类需要继承WebSecurityConfigurerAdapter类,并通过注解@EnableWebSecurity来启用Spring Security。

    本文的用户认证是使用Spring Security默认的基于用户名和密码的表单认证,需要在配置类中重写protected void configure(AuthenticationManagerBuilder auth)方法,并在重写的方法中指定默认从哪里获取认证用户的信息,即指定一个UserDetailsService接口的实现类。此外,还需要重写protected void configure(HttpSecurity http)方法,并在重写的方法中进行一系列的安全配置。本示例的配置类WebSecurityConfig代码如下所示:

    package com.authentication.server.config;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.crypto.password.PasswordEncoder;
    
    /**
     * Spring Security配置类
     */
    @EnableWebSecurity
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Autowired
        private UserDetailsService userDetailsServiceImpl;
    
        /**
         * 用户认证配置
         */
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            /**
             * 指定用户认证时,默认从哪里获取认证用户信息
             */
            auth.userDetailsService(userDetailsServiceImpl);
        }
    
        /**
         * Http安全配置
         */
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            /**
             * 表单登录:使用默认的表单登录页面和登录端点/login进行登录
             * 退出登录:使用默认的退出登录端点/logout退出登录
             * 记住我:使用默认的“记住我”功能,把记住用户已登录的Token保存在内存里,记住30分钟
             * 权限:除了/toHome和/toUser之外的其它请求都要求用户已登录
             * 注意:Controller中也对URL配置了权限,如果WebSecurityConfig中和Controller中都对某文化URL配置了权限,则取较小的权限
             */
            http
                .formLogin()
                    .defaultSuccessUrl("/toHome", false)
                    .permitAll()
                    .and()
                .logout()
                    .permitAll()
                    .and()
                .rememberMe()
                    .tokenValiditySeconds(1800)
                    .and()
                .authorizeRequests()
                    .antMatchers("/toHome", "/toUser")
                    .permitAll()
                    .anyRequest()
                    .authenticated();
        }
    
        /**
         * 密码加密器
         */
        @Bean
        public PasswordEncoder passwordEncoder() {
            /**
             * BCryptPasswordEncoder:相同的密码明文每次生成的密文都不同,安全性更高
             */
            return new BCryptPasswordEncoder();
        }
    
    }
    

    Spring Security进行用户认证时,需要根据用户的账号、密码、权限等信息进行认证,因此,需要根据查询到的用户信息封装成一个认证用户对象并交给Spring Security进行认证。查询用户信息并封装成认证用户对象的过程是在UserDetailsService接口的实现类(需要用户自己实现)中完成的。本示例的UserDetailsService接口实现类UserDetailsServiceImpl的代码如下所示:

    package com.authentication.server.service.impl;
    
    import com.authentication.server.model.AuthUser;
    import com.authentication.server.service.AuthUserService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.authority.SimpleGrantedAuthority;
    import org.springframework.security.core.userdetails.User;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.core.userdetails.UsernameNotFoundException;
    import org.springframework.stereotype.Component;
    import org.springframework.util.StringUtils;
    
    import java.util.ArrayList;
    import java.util.List;
    
    /**
     * 自定义的认证用户获取服务类
     */
    @Component("userDetailsServiceImpl")
    public class UserDetailsServiceImpl implements UserDetailsService {
    
        @Autowired
        private AuthUserService authUserServiceImpl;
    
        /**
         * 根据用户名获取认证用户信息
         */
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            if(StringUtils.isEmpty(username)) {
                throw new UsernameNotFoundException("UserDetailsService没有接收到用户账号");
            } else {
                /**
                 * 根据用户名查找用户信息
                 */
                AuthUser authUser = authUserServiceImpl.getAuthUserByUsername(username);
                if(authUser == null) {
                    throw new UsernameNotFoundException(String.format("用户'%s'不存在", username));
                }
                List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
                for (String role : authUser.getRoles()) {
                    //封装用户信息和角色信息到SecurityContextHolder全局缓存中
                    grantedAuthorities.add(new SimpleGrantedAuthority(role));
                }
                /**
                 * 创建一个用于认证的用户对象并返回,包括:用户名,密码,角色
                 */
                return new User(authUser.getUsername(), authUser.getPassword(), grantedAuthorities);
            }
        }
    }
    

    查询用户信息的接口AuthUserService 的代码如下所示:

    package com.authentication.server.service;
    
    import com.authentication.server.model.AuthUser;
    
    /**
     * 用户服务类
     */
    public interface AuthUserService {
    
        /**
         * 通过用户账号获取认证用户信息
         */
        AuthUser getAuthUserByUsername(String username);
    
    }
    

    查询用户信息的接口实现类AuthUserServiceImpl的代码如下所示:

    package com.authentication.server.service.impl;
    
    import com.authentication.server.model.AuthUser;
    import com.authentication.server.service.AuthUserService;
    import org.springframework.stereotype.Service;
    
    import java.util.ArrayList;
    import java.util.List;
    
    /**
     * 用户服务实现类
     */
    @Service
    public class AuthUserServiceImpl implements AuthUserService {
    
        /**
         * 通过用户账号获取用户信息
         */
        @Override
        public AuthUser getAuthUserByUsername(String username) {
            /**
             * 实际上这里应该是从数据库中查询或者是调用其它服务接口获取,
             * 为了方便,这里直接创建用户信息
             * admin用户拥有 ROLE_ADMIN 和 ROLE_EMPLOYEE 这两个角色
             * employee用户拥有 ROLE_EMPLOYEE 这个角色
             * temp用户没有角色
             */
            if(username.equals("admin")) {
                AuthUser user = new AuthUser();
                user.setId(1L);
                user.setUsername("admin");
                /**
                 * 密码为123(通过BCryptPasswordEncoderl加密后的密文)
                 */
                user.setPassword("$2a$10$U6g06YmMfRJXcNfLP28TR.xy21u1A5kIeY/OZMKBDVMbn7PGJiaZS");
                List<String> roles = new ArrayList<>();
                roles.add("ROLE_ADMIN");
                roles.add("ROLE_EMPLOYEE");
                user.setRoles(roles);
                return user;
            } else if(username.equals("employee")) {
                AuthUser user = new AuthUser();
                user.setId(2L);
                user.setUsername("employee");
                /**
                 * 密码为123(通过BCryptPasswordEncoderl加密后的密文)
                 */
                user.setPassword("$2a$10$U6g06YmMfRJXcNfLP28TR.xy21u1A5kIeY/OZMKBDVMbn7PGJiaZS");
                List<String> roles = new ArrayList<>();
                roles.add("ROLE_EMPLOYEE");
                user.setRoles(roles);
                return user;
            } else if (username.equals("temp")) {
                AuthUser user = new AuthUser();
                user.setId(3L);
                user.setUsername("temp");
                /**
                 * 密码为123(通过BCryptPasswordEncoderl加密后的密文)
                 */
                user.setPassword("$2a$10$U6g06YmMfRJXcNfLP28TR.xy21u1A5kIeY/OZMKBDVMbn7PGJiaZS");
                List<String> roles = new ArrayList<>();
                user.setRoles(roles);
                return user;
            } else {
                return null;
            }
        }
    
    }
    

    用户信息实体类如下所示:

    package com.authentication.server.model;
    
    import java.util.List;
    
    /**
     * 用户实体类
     */
    public class AuthUser {
        /** 用户ID */
        private Long id;
        /** 用户账号 */
        private String username;
        /** 账号密码 */
        private String password;
        /** 角色集合 */
        private List<String> roles;
    
        public Long getId() {
            return id;
        }
    
        public void setId(Long id) {
            this.id = id;
        }
    
        public String getUsername() {
            return username;
        }
    
        public void setUsername(String username) {
            this.username = username;
        }
    
        public String getPassword() {
            return password;
        }
    
        public void setPassword(String password) {
            this.password = password;
        }
    
        public List<String> getRoles() {
            return roles;
        }
    
        public void setRoles(List<String> roles) {
            this.roles = roles;
        }
    }
    

    用户认证成功之后,可以通过@AuthenticationPrincipal注解来获取认证用户信息,本示例中获取认证用户信息的web入口类UserController的代码如下所示:

    package com.authentication.server.controller;
    
    import org.springframework.security.core.annotation.AuthenticationPrincipal;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import java.security.Principal;
    
    /**
     * 用户接口类(返回JSON)
     */
    @RestController
    public class UserController {
    
        /**
         * 获取登录后的Principal(需要登录)
         */
        @GetMapping("/getPrincipal")
        public Object getPrincipal(@AuthenticationPrincipal Principal principal){
            return principal;
        }
    
        /**
         * 获取登录后的UserDetails(需要登录)
         */
        @GetMapping("/getUserDetails")
        public Object getUserDetails(@AuthenticationPrincipal UserDetails userDetails) {
            return userDetails;
        }
    
    }
    

    2.3 权限控制

    Spring Security提供了默认的权限控制功能,需要预先分配给用户特定的权限,并指定各项操作执行所要求的权限。用户请求执行某项操作时,Spring Security会先检查用户所拥有的权限是否符合执行该项操作所要求的权限,如果符合,才允许执行该项操作,否则拒绝执行该项操作。

    本示例中使用的是Spring Security提供的方法级别的权限控制,即根据权限来控制用户是否能够请求某个方法。首先,需要在工程的主启动类中使用注解@EnableGlobalMethodSecurity(prePostEnabled = true)来启动方法级别的权限控制,并指定是在方法执行之前进行权限验证;然后,需要在方法的入口处通过注解@PreAuthorize()来指定执行对应方法需要什么样的权限。

    本示例的主启动类AuthenticationServerApplication的代码如下所示:

    package com.authentication.server;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
    
    /**
     * 主启动类
     */
    @ComponentScan("com.authentication.server.*")
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    @SpringBootApplication
    public class AuthenticationServerApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(AuthenticationServerApplication.class, args);
        }
    
    }
    
    

    本示例中使用的是基于角色的权限控制,即验证用户所拥有的角色是否符合执行某个方法所需要的角色,如果符合,才允许执行该方法,否则拒绝执行该方法。需要在方法入口处通过注解 @PreAuthorize(“hasRole(‘角色名称’)”)来指定执行对应方法需要什么角色,并且是在执行对应方法之前进行角色验证。

    本示例的方法入口控制类PageController的代码如下所示:

    package com.authentication.server.controller;
    
    import org.springframework.security.access.prepost.PreAuthorize;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    
    /**
     * 页面接口类(页面跳转)
     */
    @Controller
    public class PageController {
    
        /**
         * 跳转到admin.html页面(需要登录,且需要ROLE_ADMIN角色)
         */
        @GetMapping("/toAdmin")
        @PreAuthorize("hasRole('ROLE_ADMIN')")
        public String toAdmin() {
            return "admin.html";
        }
    
        /**
         * 跳转到employee.html页面(需要登录,且需要ROLE_EMPLOYEE角色)
         */
        @GetMapping("/toEmployee")
        @PreAuthorize("hasRole('ROLE_EMPLOYEE')")
        public String toEmployee() {
            return "employee.html";
        }
    
        /**
         * 跳转到employee.html页面(需要登录,但不需要角色)
         * 注意:虽然WebSecurityConfig中配置了/toUser不需要登录,但是这里配置的权限更小,因此,/toUser以这里的配置为准
         */
        @GetMapping("/toUser")
        @PreAuthorize("isAuthenticated()")
        public String toUser() {
            return "user.html";
        }
    
        /**
         * 跳转到home.html页面(需要登录,但不需要角色)
         * 注意:虽然这里配置了/toAbout不需要登录,但WebSecurityConfig中配置的权限更小,因此,/toAbout以WebSecurityConfig中配置的为准
         */
        @RequestMapping("/toAbout")
        @PreAuthorize("permitAll")
        public String toAbout() {
            return "about.html";
        }
    
    
        /**
         * 跳转到home.html页面(不需要登录)
         */
        @RequestMapping("/toHome")
        public String toHome() {
            return "home.html";
        }
    
    }
    

    静态页面admin.html的代码如下所示:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>admin页面</title>
    </head>
    <body>
        <h1>这是Admin页面(需要登录,且需要ROLE_ADMIN角色)</h1>
    </body>
    </html>
    

    静态页面employee.html、user.html、home.html、about.html的代码与admin.html的相似,就不一一展示。

    3 功能测试

    3.1 用户认证功能测试

    运行AuthenticationServerApplication主启动类以启动项目,然后通过浏览器访问以下地址请求toUser()方法(即访问user.html页面),由于toUser()方法需要用户经过认证之后才能访问,因此,会自动跳转到用户认证页面(如下图所示)进行认证:

    http://localhost:8080/toUser
    

    在这里插入图片描述
    输入用户名admin和密码123并点击Sign in按钮,认证成功后会自动请求/toHome路径并跳转到自定义的认证成功跳转页面home.hmtl(如下图所示):
    在这里插入图片描述
    通过浏览器访问以下地址可以查看到认证用户信息如下图所示:

    http://localhost:8080/getPrincipal
    http://localhost:8080/getUserDetails
    

    在这里插入图片描述
    在这里插入图片描述

    3.2 “记住我” 功能测试

    用户进行认证时,如果勾选了用户认证页面中的Remember me on this computer选项,则当用户关闭浏览器之后,系统会记住该用户一段时间(由设置的有效期决定,本示例中是1800秒),如果在这段时间之内,当用户重新访问该系统时,用户不需要重新进行认证就已经是已认证的状态。

    使用用户名admin和密码123进行用户认证时勾选上用户认证页面中的Remember me on this computer选项,然后重复执行关闭浏览器后再访问以下地址,并观察关闭了浏览器之后,再重新访问时是跳转到认证页面(未认证状态)还是直接跳转到了user.html页面(已认证状态):

    http://localhost:8080/toUser
    

    3.3 退出功能测试

    用户认证成功之后,用户就处于已认证的状态,就可以在权限之内访问系统,此时可以通过访问以下地址请求退出已认证状态:

    http://localhost:8080/logout
    

    首先会跳转到退出确认页面(如下图所示),用户点击了Log Out按钮之后才会真正的执行退出操作,即回到未认证状态。
    在这里插入图片描述

    3.4 权限控制功能测试

    用户在未认证的情况下通过浏览器访问以下地址请求toAdmin()方法(即访问admin.html页面),由于该方法要求用户已认证且具有ROLE_ADMIN权限才能访问,因此会自动跳转到用户认证页面。

    http://localhost:8080/toAdmin
    

    此时如果用用户名employee和密码123进行认证,认证成功之后由于该用户没有ROLE_ADMIN权限,因此会自动跳转到没有权限的页面(如下图所示):
    在这里插入图片描述
    如果用用户名admin和密码123进行认证,认证成功之后由于该用户拥有ROLE_ADMIN权限,因此会自动跳转到admin.html页面(如下图所示):
    在这里插入图片描述
    可见,确实起到了权限控制的作用。

    4 原理分析

    4.1 用户认证的过滤器链

    Spring Security的用户认证流程是由一系列的过滤器链来实现的,默认的关于用户认证的过滤器链大致如下图所示:
    在这里插入图片描述

    • SecurityContextPersistenceFilter: 在请求开始时,从配置好的 SecurityContextRepository 中获取 SecurityContext,并设置给 SecurityContextHolder。在请求完成后,把 SecurityContextHolder 所持有的SecurityContext 保存到配置好的 SecurityContextRepository,同时清除 securityContextHolder 所持有的 SecurityContext。

    • UsernamePasswordAuthenticationFilter: 用于处理来自表单提交的认证。该表单必须提供用户名和密码,其内部还有登录成功或失败后的处理器 AuthenticationSuccessHandler 和 AuthenticationFailureHandler。

    • ExceptionTranslationFilter: 能够捕获过滤器链中产生的所有异常,但只处理两类异常:AuthenticationException 和 AccessDeniedException,而其它的异常则继续抛出。
      如果捕获到的是 AuthenticationException,那么将会使用其对应的 AuthenticationEntryPoint 的commence()方法进行处理。在处理之前,ExceptionTranslationFilter会先使用 RequestCache 将当前的HttpServerletRequest的信息保存起来,以至于用户登录成功后可以跳转到之前的界面。
      如果捕获到的是 AccessDeniedException,那么将会根据当前访问的用户是否已经登录认证而做不同的处理,如果未登录,则使用关联的 AuthenticationEntryPoint 的 commence()方法进行处理,否则使用关联的 AccessDeniedHandler 的handle()方法进行处理。

    • FilterSecurityInterceptor: 用于保护HTTP资源的,它需要一个 AuthenticationManager 和一个 AccessDecisionManager 的引用。它会从 SecurityContextHolder 中获取 Authentication,然后通过 SecurityMetadataSource 可以得知当前请求是否在请求受保护的资源。对于请求那些受保护的资源,如果 Authentication.isAuthenticated() 返回false(即用户未认证),或者FilterSecurityInterceptor 的 alwaysReauthenticate 属性的值为 true,那么将会使用其引用的 AuthenticationManager 对Authentication进行认证,认证之后再使用认证后的 Authentication 替换 SecurityContextHolder 中原有的那个。然后使用 AccessDecisionManager 对用户当前请求进行权限检查。

    4.2 用户认证的流程

    Spring Security支持多种用户认证的方式,最常用的是基于用户名和密码的用户认证方式,其认证流程如下图所示:
    在这里插入图片描述

    4.3 “记住我” 功能的流程

    用户可以使用账号和密码进行认证,但是如果用户使用账号和密码进行认证时选择了“记住我”功能,则在有效期内,当用户关闭浏览器后再重新访问服务时,不需要用户再次输入账号和密码重新进行认证,而是通过“记住我”功能自动认证。

    “记住我”功能的认证流程如下图所示:
    在这里插入图片描述
    上述的用户认证处理逻辑都是基于Spring Security提供的默认实现,我们只需要自己实现一个UserDetailsService接口用于获取用户认证信息即可,十分简便。当然,Spring Security也能够支持我们使用自定义的用户认证处理逻辑,我们可以自己实现AuthenticationFilter和AuthenTicationProvider,以达到按照需求进行用户认证的目的。博主的另外一篇文章会专门分享自定义用户认证的实现。

    4.4 权限控制的原理

    Spring Security允许我们通过Spring EL权限验证表达式来指定访问URL或方法所需要的权限,用户在访问某个URL或方法时,如果对应的权限验证表达式返回结果为true,则表示用户拥有访问该URL或方法的权限,如果返回结果为false,则表示没有权限。Spring Security为我们提供了以下的权限验证表达式:

    表达式描述
    hasRole([role])当前用户是否拥有指定角色。
    hasAnyRole([role1,role2])多个角色是一个以逗号进行分隔的字符串。如果当前用户拥有指定角色中的任意一个则返回true。
    hasAuthority([auth])等同于hasRole
    hasAnyAuthority([auth1,auth2])等同于hasAnyRole
    Principle代表当前用户的principle对象
    authentication直接从SecurityContext获取的当前Authentication对象
    permitAll总是返回true,表示允许所有的
    denyAll总是返回false,表示拒绝所有的
    isAnonymous()当前用户是否是一个匿名用户
    isRememberMe()表示当前用户是否是通过Remember-Me自动登录的
    isAuthenticated()表示当前用户是否已经登录认证成功了。
    isFullyAuthenticated()如果当前用户既不是一个匿名用户,同时又不是通过Remember-Me自动登录的,则返回true。

    权限验证表达式只能验证用户是否具有访问某个URL或方法的权限,但是权限验证的这个步骤可以在不同的阶段进行。Spring Security中定义了以下四个支持使用权限验证表达式的注解,其中前两者可以用来在方法调用前或者调用后进行权限验证,后两者可以用来对集合类型的参数或者返回值进行过滤:

    • @PreAuthorize
    • @PostAuthorize
    • @PreFilter
    • @PostFilter

    权限验证表达式需要和注解结合使用,示例如下所示:

    @PreAuthorize("hasRole('ROLE_ADMIN')")
    public void addUser(User user) {
       ...
    }
    
    @PreAuthorize("hasRole('ROLE_USER') or hasRole('ROLE_ADMIN')")
    public User find(int id) {
       return null;
    }
    
    @PreAuthorize("#id<10")
    public User find(int id) {
       return null;
    }
    
    @PreAuthorize("principal.username.equals(#username)")
    public User find(String username) {
       return null;
    }
    
    @PreAuthorize("#user.name.equals('abc')")
    public void add(User user) {
       ...
    }
    
    @PostAuthorize("returnObject.id%2==0")
    public User find(int id) {
       ...
       return user;
    }
    
    @PostFilter("filterObject.id%2==0")
    public List<User> findAll() {
       List<User> userList = new ArrayList<User>();
       ...
       return userList;
    }
    
    @PreFilter(filterTarget="ids", value="filterObject%2==0")
    public void delete(List<Integer> ids, List<String> usernames) {
       ...
    }
    

    5 总结

    本文从使用方法和原理分析这两个方法简要的介绍了Spring Security的用户认证和权限控制这两大功能,但都是基于Spring Security的默认实现,我们也可以自定义用户认证和权限控制的实现逻辑,Spring Security用户认证和权限控制(自定义实现)详细介绍了用户认证相关和权限控制相关的自定义实现。关于授权服务器、资源服务器的内容可以查阅以下几篇文章:

    OAuth2授权服务器和四种授权方式 这篇文章介绍了授权服务器和四种授权方式的配置与使用方法。
    OAuth2资源服务器 这篇文章介绍了基于方法级别的权限控制的资源服务器的配置与使用方法。

    如果觉得本文对您有帮助,请关注博主的微信公众号,会经常分享一些Java和大数据方面的技术案例!
    在这里插入图片描述

    展开全文
  • 统一用户认证和单点登录和授权的原理与流程

    千次阅读 多人点赞 2019-02-02 13:58:09
    彻底搞懂统一用户认证和用户单点登录1. 前言2. 原理1. 统一用户认证介绍2. 单点登录原理介绍3. OAuth 2.0的统一用户认证1. OAuth 2.0协议和流程简介2. 授权码模式3. 简化模式4. 密码模式5. 客户端模式6. 授权码模式...

    1 前言

    随着信息技术和网络技术的迅猛发展,大型企业内部的应用系统越来越多。比如在电商行业,常见的应用系统就有商品系统、订单系统、支付系统、物流系统、客服系统等等。这些系统互相独立,且每个系统都需要识别用户的身份,并根据用户的身份来进行权限控制。如果每个系统都各自实现用户管理和认证的功能,就会带来用户信息冗余、不同步等问题,也会导致用户必须记住每一个系统的用户名和密码,从而给用户带来不少麻烦。针对这些实际情况,统一用户认证、单点登录、授权等概念应运而生,而且被广泛应用到企业应用系统中。

    本文主要介绍基于Spring Security + OAuth 2.0协议的统一用户认证、单点登录、授权的原理和流程。

    2 介绍

    2.1 统一用户认证

    统一用户认证,就是判断一个用户是否为合法用户的过程。一般来说,大型企业内部的每个应用系统都需要识别用户的身份,如果每个系统都各自实现用户管理和认证的功能,那么对于企业而言,必定会增加用户信息的管理成本,也会导致多个系统之间的用户信息冗余且不同步的问题,对于用户而言,必须记住每个系统的账户信息,从而带来不便。例如,用户同时使用某个大型企业内部的10个应用系统,就必须在这10个应用系统中都创建自己的账户并记住账户,这样每个系统都需要管理用户的信息,而且一旦用户在某个系统中修改了信息,就会导致系统之间用户信息不一致的问题。万一系统的数量增加到50个、100个了?估计企业和用户都要疯了。

    解决上述问题的根本方法是建立统一用户管理系统(UUMS)。UUMS统一存储、维护、管理所有应用系统的用户信息,所有系统的用户认证功能全部由UUMS来完成,而用户权限控制功能则由各个应用系统自己完成。可见、UUMS应具备以下基本功能:

    1. 统一用户管理:用户信息的存储、维护、管理等功能。
    2. 统一用户认证:用户身份识别、角色分配等功能。

    统一用户认证是以UUMS为基础,对所有的系统提供统一的认证方式和认证策略,以识别用户身份的合法性。

    注意:统一用户认证是为了解决多个系统的用户管理和认证的问题。

    2.2 单点登录

    对于大型企业内部的多个应用系统而言,统一用户管理系统能够很好的解决用户信息冗余和不同步的问题,也能减轻用户需要创建并管理多个账户的痛苦,即使这样也仍然存在多个系统都需要用户登录的问题。例如,用户同时使用某个大型企业内部的10个应用系统,那么就需要用户使用同一个账号信息去分别登录每个系统,这样的操作必定会导致用户体验太差。单点登录就是为了解决这个问题而提出的。

    单点登录(Single Sign On,简称SSO)是一种方便用户访问多个系统的技术,用户只需在一个系统中进行登录,就可以在多个系统间自由穿梭,不必重复输入用户名和密码来确定身份。

    注意:单点登录是为了解决同一用户在使用多个系统时的重复登录的问题。

    2.3 授权

    我们经常会遇到这样一个问题:在访问某些系统(例如知乎、豆瓣),都需要用户先注册账号,然后登录才能继续访问,而很多用户却又嫌注册太麻烦,为了方便用户使用,这些系统都会提供使用第三方账号(例如微信账号)进行登录的方式。那么问题来了,如果用户直接将自己的第三方账号和密码告诉这些系统,那么又怎样确保这些系统不会泄露用户的账号信息以及私自获取用户在第三方系统中除用户账户之外的数据?“授权”就能很好的解决这个问题。

    所谓“授权”,就是用户先授予某个系统可以使用自己的第三方账号信息的权利,然后该系统就可以使用用户的授权到第三方去获取该用户的账号信息,而第三方验证了用户的授权之后就按照用户授权的范围把用户允许的账号信息返回给该系统,而不会把用户没有允许的数据返回给该系统。这样一来,用户既可以访问这些系统,又不需要把自己的第三方账号信息告诉给这些系统,从而可以解决上述问题。

    注意:授权是为了解决多个系统之间用户信息共享的问题。

    3 原理

    3.1 统一用户认证原理

    统一用户认证的原理相对简单,基本都是判断当前正在操作的用户的账户是否存在且密码是否匹配。如果用户认证通过了,那么用户就可以使用预先设定的角色来进行操作了,至于该角色能够进行哪些操作,则由各个应用系统自己指定了。统一用户认证包括以下几种认证方式,这些认证方式采用模块化设计,可以根据需求而组合使用:

    1. 匿名认证方式:用户不需要任何认证,可以匿名的方式登录系统。
    2. 用户名/密码认证:这是最基本的认证方式。
    3. PKI/CI数字证书认证:通过数字证书的方式认证用户的身份。
    4. IP地址认证:用户只能用指定的IP地址或IP地址段访问系统。
    5. 时间段认证:用户只能在指定的时间段访问系统。
    6. 访问次数认证:累计用户的访问次数,使用户的访问次数在一定的范围之内才可以访问系统。

    Spring Security和Shiro是两种主流的统一用户认证框架。

    3.2 单点登录原理

    单点登录的原理就是:用户在使用多个应用系统时,当用户第一次访问任一应用系统时,会因为没有登录而被引导到用户认证系统中进行认证,如果认证通过,用户就会得到一个认证凭据——ticket,用户再访问其它应用系统时,就会带上这个ticket作为自己已认证的凭据,而其它应用系统校验ticket通过之后,用户就可以不需要再次认证就可以直接该其它应用系统了。

    要实现单点登录,需要以下主要功能:

    1. 所有的应用系统共享一个用户认证系统:统一的认证系统是单点登录的前提之一。
    2. 所有的应用系统能够识别和提取ticket信息:应用系统应该能对ticket进行识别和提取,从而判断当前用户是否已经认证,从而完成单点登录的功能。

    单点登录的实现方案较多,有Spring Security + OAuth方案、Spring Security + CAS方案、Shiro + CAS方案,但Spring Security + OAuth方案更常用,本文讲解的单点登录也是基于这种方案。

    3.3 OAuth授权原理

    OAuth是一种授权协议,OAuth协议区分以下四种角色:

    1. 资源拥有者:即用户
    2. 第三方应用:即客户端
    3. 认证服务器:即服务提供商专门用来处理用户统一认证的服务器
    4. 资源服务器:即服务提供商专门用来存放用户生成的资源的服务器。它与认证服务器,可以是同一台服务器,也可以是不同的服务器。

    OAuth 2.0支持四种授权模式:

    1. 授权码授权模式(Authorization Code Grant)
    2. 简化授权模式(Implicit Grant)
    3. 密码授权模式(Resource Owner Password Credentials Grant)
    4. 客户端凭证授权模式(Client Credentials Grant)

    4 OAuth 2.0的四种授权模式

    4.1 授权码授权模式流程(重点)

    授权码模式是最严格也是最重要的一种授权模式,其主要流程如下图所示:
    在这里插入图片描述

    1. 用户访问:用户访问某个应用系统的客户端
    2. 请求授权:客户端请求用户授权,允许自己获取和使用用户的特定信息数据
    3. 同意授权:用户同意授权给客户端
    4. 申请授权码:客户端使用用户的授权到服务提供商的认证服务器申请授权码
    5. 发放授权码:认证服务器校验客户端使用的用户授权,校验通过则向客户端发放授权码
    6. 申请令牌:客户端使用用户的授权到服务提供商的认证服务器申请令牌
    7. 发放令牌:认证服务器校验客户端使用的用户授权,校验通过则向客户端发放令牌
    8. 申请资源:客户端使用令牌到服务提供商的资源服务器申请用户的特定信息数据
    9. 发放资源:资源服务器校验客户端使用的令牌,校验通过则将用户指定的用户信息数据发送给客户端

    注意: 授权码授权模式既适用于同一企业的多个系统之间的授权,也适用于不同企业的多个系统之间的授权

    4.2 授权码授权模式详细示例

    这里以知乎客户端、微信认证服务器、微信资源服务器为例 ,这三个相互独立,其中认证服务器使用授权码模式对所有的微信用户进行认证,所有微信用户的数据全部保存在微信资源服务器中,而知乎客户端需要用户登录之后才能进行操作,那么用户使用微信授权知乎客户端进行操作的完整流程如下图所示:
    在这里插入图片描述

    1. 用户访问:用户在未登录知乎客户端的情况下,点击知乎客户端中的链接发送请求
    2. 请求授权:知乎客户端发现用户点击的是需要登录才能访问的链接,于是将用户导向微信认证服务器进行认证并授权
    3. 同意授权:用户在微信认证服务器中输入微信账号信息进行认证,并进行授权
    4. 发放授权码:微信认证服务器验证用户微信账号的合法性和是否授权,如果用户账号合法且确定授权,则将授权码发送给知乎客户端
    5. 申请令牌:知乎客户端收到授权码后,使用收到的授权码到微信认证服务器换取令牌
    6. 发放令牌:微信认证服务器验证知乎客户端发送过来的授权码的正确性,如果通过,则将令牌发送给知乎客户端
    7. 申请资源:知乎客户端收到令牌后,使用收到的令牌到微信资源服务器获取用户微信账号的指定信息
    8. 发放资源:微信资源服务器验证知乎客户端发送过来的令牌的正确性,如果通过,则将用户微信账号的指定信息数据发送给知乎客户端
    9. 解析登录:知乎客户端收到用户微信账号的指定信息后解析并登录知乎客户端
    10. 展示数据:知乎客户端从知乎后台获取数据并展示给用户(这里涉及多步操作,省略)

    4.3 简化授权模式流程

    简化授权模式流程较授权码授权模式少了授权码这一环节,因此叫简化授权模式,其主要流程如下图所示:
    在这里插入图片描述

    1. 用户访问:用户访问某个应用系统的客户端
    2. 请求授权:客户端请求用户授权,允许自己获取和使用用户的特定信息数据
    3. 同意授权:用户同意授权给客户端
    4. 申请令牌:客户端使用用户的授权到服务提供商的认证服务器申请令牌
    5. 发放令牌:认证服务器校验客户端使用的用户授权,校验通过则向客户端发放令牌
    6. 申请资源:客户端使用令牌到服务提供商的资源服务器申请用户的特定信息数据
    7. 发放资源:资源服务器校验客户端使用的令牌,校验通过则将用户指定的用户信息数据发送给客户端

    注意: 简化授权模式既适用于同一企业的多个系统之间的授权,也适用于不同企业的多个系统之间的授权

    4.4 简化授权模式详细示例

    这里以知乎客户端、微信认证服务器、微信资源服务器为例 ,这三个相互独立,其中认证服务器使用简化模式对所有的微信用户进行认证,所有微信用户的数据全部保存在微信资源服务器中,而知乎客户端需要用户登录之后才能进行操作,那么用户使用微信授权知乎客户端进行操作的完整流程如下图所示:
    在这里插入图片描述

    1. 用户访问:用户在未登录知乎客户端的情况下,点击知乎客户端中的链接发送请求
    2. 请求授权:知乎客户端发现用户点击的是需要登录才能访问的链接,于是将用户导向微信认证服务器进行认证并授权
    3. 同意授权:用户在微信认证服务器中输入微信账号信息进行认证,并进行授权
    4. 发放令牌:微信认证服务器验证用户微信账号的合法性和是否授权,如果用户账号合法且确定授权,则将令牌发送给知乎客户端
    5. 申请资源:知乎客户端收到令牌后,使用收到的令牌到微信资源服务器获取用户微信账号的指定信息
    6. 发放资源:微信资源服务器验证知乎客户端发送过来的令牌的正确性,如果通过,则将用户微信账号的指定信息数据发送给知乎客户端
    7. 解析登录:知乎客户端收到用户微信账号的指定信息后解析并登录知乎客户端
    8. 展示数据:知乎客户端从知乎后台获取数据并展示给用户(这里涉及多步操作,省略)

    4.5 密码授权模式流程

    密码授权模式要慎重使用,因为用户需要直接将账号和密码等敏感信息提供给客户端,除非客户端是整个系统的一部分,或者是值得信赖的第三方系统,才可以考虑这种授权方式。其主要流程如下图所示:
    在这里插入图片描述

    1. 用户访问:用户访问某个应用系统的客户端
    2. 要求登录:客户端要求用户登录
    3. 提供账户:用户向客户端提供账号和密码进行登录
    4. 申请令牌:客户端使用用户的账号和密码到服务提供商的认证服务器申请令牌
    5. 发放令牌:认证服务器校验客户端使用的用户授权,校验通过则向客户端发放令牌
    6. 申请资源:客户端使用令牌到服务提供商的资源服务器申请用户的特定信息数据
    7. 发放资源:资源服务器校验客户端使用的令牌,校验通过则将用户指定的用户信息数据发送给客户端

    注意: 密码授权模式主要适用于客户端属于系统的一部分的情况

    4.6 密码授权模式详细示例

    这里以新浪微博客户端、新浪微博认证服务器、新浪微博资源服务器为例 ,这三个共同组成新浪微博系统,其中认证服务器使用密码模式对所有的新浪微博用户进行认证,所有新浪微博用户的数据全部保存在新浪微博资源服务器中,而新浪微博客户端需要用户登录之后才能进行操作,那么用户使用新浪微博客户端进行操作的完整流程如下图所示:
    在这里插入图片描述

    1. 用户访问:用户在未登录新浪微博客户端的情况下,点击新浪微博客户端中的链接发送请求
    2. 请求登录:新浪微博客户端发现用户点击的是需要登录才能访问的链接,于是将用户导新浪微博客户端的登录页面进行登录
    3. 用户登录:用户在新浪微博客户端的登录页面输入新浪微博账号信息进行登录(登录成功即授权成功)
    4. 申请令牌:新浪微博客户端将用户输入的新浪微博账号信息发送到新浪微博认证服务器进行用户认证
    5. 发送令牌:新浪微博认证服务器验证新浪微博客户端发送过来的用户新浪微博账号的合法性,如果用户账号合法,则将令牌发送给新浪微博客户端
    6. 申请资源:新浪微博客户端收到令牌后,使用收到的令牌到新浪微博资源服务器获取用户新浪微博账号信息
    7. 发送资源:新浪微博资源服务器验证新浪微博客户端发送过来的令牌的正确性,如果通过,则将用户新浪微博账号信息数据发送给新浪微博客户端
    8. 解析登录:新浪微博客户端收到用户新浪微博账号信息后解析并登录新浪微博客户端
    9. 展示数据:新浪微博客户端从新浪微博资源服务器获取用户请求的数据并展示给用户(这里涉及多步操作,省略)

    4.7 客户端授权模式流程

    这种授权模式比较特殊,客户端不是以用户的名义向认证服务器申请令牌,而是以自己的名义向认证服务器申请令牌,因此,这种授权模式其它与用户没有任何关系。其主要流程如下图所示:
    在这里插入图片描述

    1. 用户访问:用户访问某个应用系统的客户端
    2. 申请令牌:客户端使用自己的名义到服务提供商的认证服务器申请令牌
    3. 发放令牌:认证服务器校验客户端是否是合法的指定客户端,校验通过则向客户端发放令牌
    4. 申请资源:客户端使用令牌到服务提供商的资源服务器申请数据资源
    5. 发放资源:资源服务器校验客户端使用的令牌,校验通过则将客户端申请的数据资源发送给客户端

    注意: 客户端授权模式主要适用于客户端和系统之间默认存在信任关系的情况

    4.8 客户端授权模式详细示例

    这里以市天气系统客户端、省天气系统认证服务器、省天气系统资源服务器为例 ,其中省天气系统认证服务器使用客户端模式对所有的市天气系统客户端进行认证,该省所有市的的天气数据全部保存在省天气系统资源服务器中,那么用户使用市天气系统客户端查看当市天气的完整流程如下图所示:
    在这里插入图片描述

    1. 用户请求:用户在市天气系统客户端中点击链接,请求查看当市天气
    2. 申请令牌:市天气系统客户端将自己的凭证发送给省天气系统认证服务器进行身份认证
    3. 发放令牌:省天气系统认证服务器验证市天气系统客户端发送过来的凭证的合法性,如果凭证合法,则将令牌发送给市天气系统客户端
    4. 申请资源:市天气系统客户端收到令牌后,使用收到的令牌到省天气系统资源服务器获取当市天气
    5. 发放资源:省天气系统资源服务器验证市天气系统客户端发送过来的令牌的正确性,如果通过,则将该市天气数据发送给市天气系统客户端
    6. 展示数据:市天气系统客户端将收到的数据展示给用户

    5 OAuth 2.0单点登录

    单点登录是指在使用多个系统时,只需一次登录就可以使用所有系统,可见,单点登录是针对多个系统而言的,而这多个系统,可能同属于某一个企业(或机构),也可能分别属于不同的企业(或机构),而从2.2小节可知,单点登录的前提是统一用户认证,因此,单点登录主要是结合授权码授权和简单授权这两模式使用。以下两个示例则以授权码授权模式展示两个系统的单点登录流程,多个系统的单点登录流程依此类推。

    5.1 同一企业内部多系统的单点登录示例

    这里以淘宝客户端、天猫客户端、阿里巴巴认证服务器、阿里巴巴资源服务器为例 ,这四个相互独立但同属于阿里巴巴集团,其中认证服务器使用授权码模式对所有的阿里巴巴用户进行认证,所有阿里巴巴用户的数据全部保存在阿里巴巴资源服务器中,而淘宝客户端和天猫客户端需要用户登录之后才能进行操作,那么阿里巴巴用户基于单点登录使用淘宝客户端和天猫客户端进行操作的完整流程如下图所示:
    在这里插入图片描述

    1. 用户访问:用户在未登录淘宝客户端的情况下,点击淘宝客户端中的链接发送请求
    2. 请求认证:淘宝客户端发现用户点击的是需要登录才能访问的链接,于是将用户导向阿里巴巴认证服务器进行认证
    3. 用户认证:用户在阿里巴巴认证服务器中输入账号信息进行认证(认证通过即授权成功)
    4. 发放授权码:阿里巴巴认证服务器验证用户账号的合法性,如果账号合法,则将授权码发送给淘宝客户端
    5. 申请令牌:淘宝客户端收到授权码后,使用收到的授权码到阿里巴巴认证服务器换取令牌
    6. 发放令牌:阿里巴巴认证服务器验证淘宝客户端发送过来的授权码的正确性,如果通过,则将令牌发送给淘宝客户端
    7. 申请资源:淘宝客户端收到令牌后,使用收到的令牌到阿里巴巴资源服务器获取用户账号信息
    8. 发放资源:阿里巴巴资源服务器验证淘宝客户端发送过来的令牌的正确性,如果通过,则将用户账号信息数据发送给淘宝客户端
    9. 解析登录:淘宝客户端收到用户账号信息后解析并登录淘宝客户端
    10. 展示数据:淘宝客户端从阿里巴巴资源服务器获取用户请求所需要的数据并展示给用户(这里涉及多步操作,省略)
      ——————————————————————————————————————————————
    11. 用户访问:随后用户在未登录天猫客户端的情况下,点击天猫客户端中的链接发送请求
    12. 请求认证:天猫客户端发现用户点击的是需要登录才能访问的链接,于是将用户导向阿里巴巴认证服务器进行认证
    13. 发放授权码:阿里巴巴认证服务器发现用户已经认证,因此直接将授权码发送给天猫客户端
    14. 申请令牌:天猫客户端收到授权码后,使用收到的授权码到阿里巴巴认证服务器换取令牌
    15. 发放令牌:阿里巴巴认证服务器验证天猫客户端发送过来的授权码的正确性,如果通过,则将令牌发送给天猫客户端
    16. 申请资源:天猫客户端收到令牌后,使用收到的令牌到阿时巴巴资源服务器获取用户账号信息
    17. 发放资源:阿里巴巴资源服务器验证天猫客户端发送过来的令牌的正确性,如果通过,则将用户账号信息数据发送给天猫客户端
    18. 解析登录:天猫客户端收到用户账号信息后解析并登录天猫客户端
    19. 展示数据:天猫客户端从阿里巴巴资源服务器获取用户请求所需要的数据并展示给用户

    5.2 不同企业之间多系统的单点登录示例

    这里以知乎客户端、豆瓣客户端、微信认证服务器、微信资源服务器为例 ,这四个相互独立,但分别属于三个企业,其中微信认证服务器使用授权码模式对所有的微信用户进行认证,所有微信用户的数据全部保存在微信资源服务器中,而知乎客户端和豆瓣客户端需要用户登录之后才能进行操作,那么微信用户基于单点登录使用知乎客户端和豆瓣客户端进行操作的完整流程如下图所示:
    在这里插入图片描述

    1. 用户访问:用户在未登录知乎客户端的情况下,点击知乎客户端中的链接发送请求
    2. 请求授权:知乎客户端发现用户点击的是需要登录才能访问的链接,于是将用户导向微信认证服务器进行认证并授权
    3. 认证并授权:用户在微信认证服务器中输入微信账号信息进行认证,并进行授权
    4. 发放授权码:微信认证服务器验证用户微信账号的合法性和用户是否授权,如果账号合法且确定授权,则将授权码发送给知乎客户端
    5. 申请令牌:知乎客户端收到授权码后,使用收到的授权码到微信认证服务器换取令牌
    6. 发放令牌:微信认证服务器验证知乎客户端发送过来的授权码的正确性,如果通过,则将令牌发送给知乎客户端
    7. 申请资源:知乎客户端收到令牌后,使用收到的令牌到微信资源服务器获取用户微信账号的指定信息
    8. 发放资源:微信资源服务器验证知乎客户端发送过来的令牌的正确性,如果通过,则将用户微信账号的指定信息数据发送给知乎客户端
    9. 解析登录:知乎客户端收到用户微信账号的指定信息后解析并登录知乎客户端
    10. 展示数据:知乎客户端从知乎后台获取用户请求所需要的数据并展示给用户(这里涉及多步操作,省略)
      ——————————————————————————————————————————————
    11. 用户访问:随后用户在未登录豆瓣的情况下,点击豆瓣客户端中的链接发送请求
    12. 请求授权:豆瓣客户端发现用户点击的是需要登录才能访问的链接,于是将用户导向微信认证服务器进行认证并授权
    13. 授权:此时用户在微信认证服务器中已经认证,因此,用户只需要进行授权
    14. 发放授权码:微信认证服务器验证用户是否授权,如果确定授权,则将授权码发送给豆瓣客户端
    15. 申请令牌:豆瓣客户端收到授权码后,使用收到的授权码到微信认证服务器换取令牌
    16. 发放令牌:微信认证服务器验证豆瓣客户端发送过来的授权码的正确性,如果通过,则将令牌发送给豆瓣客户端
    17. 申请资源:豆瓣客户端收到令牌后,使用收到的令牌到微信资源服务器获取用户微信账号的指定信息
    18. 发放资源:微信资源服务器验证豆瓣客户端发送过来的令牌的正确性,如果通过,则将用户微信账号的指定信息数据发送给豆瓣客户端
    19. 解析登录:豆瓣客户端收到用户微信账号的指定信息后解析并登录豆瓣客户端
    20. 展示数据:豆瓣客户端从豆瓣后台获取用户请求所需要的数据并展示给用户(这里涉及多步操作,省略)

    5.3 比较

    从上述两个单点登录的示例可以看出,同一企业内部多系统的单点登录与不同企业之间多系统的单点登录的流程基本相同,不同的是:同一企业内部多系统的单点登录流程中,只需要用户在首次使用系统时进行用户认证,且认证成功即默认为确定授权,而随后使用其它系统时都不需要再进行认证和授权;而不同企业内部多系统的单点登录流程中,用户在首次使用系统时不仅需要进行用户认证,还要进行授权,而随后使用其它系统时虽然不需要再进行认证,但仍需要再次授权。

    6 小结

    至此,用户统一认证、单点登录、授权的原理与详细流程就全部介绍完成,欢迎聪明的Java猿们留言补充讨论。

    如果觉得本文对您有帮助,请关注博主的微信公众号,会经常分享一些Java和大数据方面的技术案例!
    在这里插入图片描述

    展开全文
  • 本篇文章主要讲解 token、session 等用户认证方案的区别并分析常见误区,以及如何通过前后端的配合实现完善的访问拦截,为下一步权限控制的实现打下基础。

    重要链接:
    「系列文章目录」

    「项目源码(GitHub)」

    前言

    之前我老说自己写文章不容易,一篇有时候要搞七八个小时,想到大多数人恐怕没我这么用心,就偷懒偷得比较心安理得。但最近刷 B 站,发现一些 UP 主居然会花上百个小时去剪一个三分钟的视频,我了个乖乖,虽说写文章不像他们发视频那样能挣钱,但是我还是被他们这种为梦想爆肝的精神感动了,活该人家成功(tū tóu)啊。不过感动归感动,对于我来说写文章是一种兴趣,要是做到这种程度恐怕就没什么幸福感了。如果每篇文章都是这样的篇幅,两周一更其实压力也并不小,毕竟除去开头结尾吹牛扯皮和代码,一篇教程最起码有三千字是要字句斟酌的。

    本篇文章主要内容如下:

    • 登出功能开发
    • 用户认证机制详解(session、token)
    • 通过前后端的配合实现完善的访问拦截
    • 进一步分析 Shiro 的工作机制

    一、登出功能开发

    有的同学可能已经发现了,我们的系统一直没有登出功能。其实按照过去的登录验证方法,服务器端并不会记住 “登录成功” 的状态,也就是说,是否执行这个方法对服务器来说并没有什么区别,用户完全可以不用登录自行构造请求访问后端的各种资源。仅仅依靠前端的一些判断,只能骗骗不懂计算机的小朋友们。

    之前的文章 「Vue + Spring Boot 项目实战(六):前端路由与登录拦截器」 中分别讲解了前端拦截和后端拦截,在我们这个前后端分离的破项目中,必须把这两种方式结合起来,才能实现真正意义上的访问控制。(这也体现了前后端分离令人蛋疼之处)

    在我们引入 Shiro 作为安全框架之后,拥有了对登录状态进行管理的能力,这时,我们才能实现真正意义上的登入和登出。登入上节已经讲过了,这里我们简单实现一下登出。

    1.后端

    后端代码如下:

        @ResponseBody
        @GetMapping("api/logout")
        public Result logout() {
            Subject subject = SecurityUtils.getSubject();
            subject.logout();
            String message = "成功登出";
            return ResultFactory.buildSuccessResult(message);
        }
    

    核心就是 subject.logout(),默认 Subject 接口是由 DelegatingSubject 类实现,其 logout 方法如下:

        public void logout() {
            try {
                this.clearRunAsIdentitiesInternal();
                this.securityManager.logout(this);
            } finally {
                this.session = null;
                this.principals = null;
                this.authenticated = false;
            }
    
        }
    

    可以看出,该方法会清除 session、principals,并把 authenticated 设置为 false。有兴趣的同学可以再看看 SecurityManager 中做了什么,这里就不赘述了。

    之前我们在后端配置了拦截器,由于登出功能不需要被拦截,所以我们还需要修改配置类 MyWebConfigureraddInterceptors() 方法,添加一条路径:

        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(getLoginIntercepter())
                    .addPathPatterns("/**")
                    .excludePathPatterns("/index.html")
                    .excludePathPatterns("/api/login")
                    .excludePathPatterns("/api/logout");
        }
    

    2.前端

    前端要做的事情有两件,一是显示,二是逻辑。原来我们一直讲前端简单后端复杂,但现在前端能做的事情太多了,谁复杂还真不好说。而且过去十几年各种后端语言此起彼伏,但 JavaScript 一枝独秀稳坐泰山,所以学它肯定不亏。虽然我们的教程讲 Vue,但是我还是想提醒一下各位兴趣在前端的小伙伴,不要只会用框架,框架一浪拍一浪死的很快,原生 JS 才是你的立身之本。

    显示这里我简单写一下,在原来顶部的导航栏里加一个登出按钮,如图:
    在这里插入图片描述
    写在 <el-menu> 标签里即可:

    <i class="el-icon-switch-button" v-on:click="logout" style="float:right;font-size: 40px;color: #222;padding: 10px"></i>
    

    调整样式:

      .el-icon-switch-button {
        cursor: pointer;
        outline:0;
      }
    

    在 methods 中编写 logout() 方法:

          logout () {
            var _this = this
            this.$axios.get('/logout').then(resp => {
              if (resp.data.code === 200) {
                // 前后端状态保持一致
                _this.$store.commit('logout')
                _this.$router.replace('/login')
              }
            })
          }
    

    在 store 中定义 logout 方法:

        logout (state) {
          state.user = []
          window.localStorage.removeItem('user')
        }
    

    这样,登出功能就开发完成了。

    二、完善的访问拦截

    现在,虽然我们登录登出的状态没问题了,但是还有关键的一步没有做。上篇文章的最后,我们说可以通过在控制台输入类似

    window.localStorage.setItem('user', JSON.stringify({"name":"哈哈哈"}));
    

    的命令来绕过前端的 “全局前置守卫”(router.beforeEach),所以要想真正实现登录拦截,必须在后端也判断用户是否登录以及登录的是哪个瓜皮用户,而这就需要前端向后端发送用户信息。

    说到这里,我感到头皮掠过一丝凉意,因为关于这个用户信息如何表示、如何存储、如何验证是一个大问题,讲明白不容易。你也许能在网上搜到许多讲这个的文章,比我写的好的有很多,但我发现瞎说的更多,所以我希望能给读者们讲清楚,不要被其它文章带跑偏了。

    1.认证方案(session 与 token)

    先说最简单的认证方法,即前端在每次请求时都加上用户名和密码,交由后端验证。这种方法的弊端有两个:

    • 一,需要频繁查询数据库,导致服务器压力较大
    • 二,安全性,如果信息被截取,攻击者就可以 一直 利用用户名密码登录(注意不是因为明文不安全,是由于无法控制时效性)

    为了在某种程度上解决上述两个问题,有两种改进的方案 —— session 与 token。

    - session

    许多语言在网络编程模块都会实现会话机制,即 session。利用 session,我们可以管理用户状态,比如控制会话存在时间,在会话中保存属性等。其作用方式通常如下:

    • 服务器接收到第一个请求时,生成 session 对象,并通过响应头告诉客户端在 cookie 中放入 sessionId
    • 客户端之后发送请求时,会带上包含 sessionId 的 cookie
    • 服务器通过 sessionId 获取 session ,进而得到当前用户的状态(是否登录)等信息

    也就是说,客户端只需要在登录的时候发送一次用户名密码,此后只需要在发送请求时带上 sessionId,服务器就可以验证用户是否登录了。

    session 存储在内存中,在用户量较少时访问效率较高,但如果一个服务器保存了几十几百万个 session 就十分难顶了。同时由于同一用户的多次请求需要访问到同一服务器,不能简单做集群,需要通过一些策略(session sticky)来扩展,比较麻烦。

    之前见过有的人把 sessionId 持久化到数据库里,只存个 id,大头还在内存里,这个操作我是看不懂的。。。

    - token

    虽然 session 能够比较全面地管理用户状态,但这种方式毕竟占用了较多服务器资源,所以有人想出了一种无需在服务器端保存用户状态(称为 “无状态”)的方案,即使用 token(令牌)来做验证。

    对于 token 的理解,比较常见的误区是:

    • 生成方面,使用随机算法生成的字符串、设备 mac 地址甚至 sessionId 作为 token。虽然从字面意思上讲这些也算是“令牌”,但是毫无意义。这是真的 “没有状态” 了,对于状态的控制甚至需要用 session 完成,那只用 session 不好吗?
    • 验证方面,把 token 存储在 session 或数据库中,比对前端传来的 token 与存储的 token 是否一致。鬼鬼,同样的骚操作。更骚的是为了提高比对效率把 token 存储在 redis 中,大家一看哇偶好高端好有道理,就直接采用这种方法了,甚至都懒得想 token 到底是个什么。。

    简单来说,一个真正的 token 本身是携带了一些信息的,比如用户 id、过期时间等,这些信息通过签名算法防止伪造,也可以使用加密算法进一步提高安全性,但一般没有人会在 token 里存储密码,所以不加密也无所谓,反正被截获了结果都一样。(一般会用 base64 编个码,方便传输)

    在 web 领域最常见的 token 解决方案是 JWT(JSON Web Token),其具体实现可以参照官方文档,这里不再赘述。

    token 的安全性类似 session 方案,与明文密码的差异主要在于过期时间。其作用流程也与 session 类似:

    • 用户使用用户名密码登录,服务器验证通过后,根据用户名(或用户 id 等),按照预先设置的算法生成 token,其中也可以封装其它信息,并将 token 返回给客户端(可以设置到客户端的 cookie 中,也可以作为 response body)
    • 客户端接收到 token,并在之后发送请求时带上它(利用 cookie、作为请求头或作为参数均可)
    • 服务器对 token 进行解密、验证

    最后再强调一下:

    token 的优势是无需服务器存储!!!
    token 的优势是无需服务器存储!!!
    token 的优势是无需服务器存储!!!

    不要再犯把 token 存储到 session 或是数据库中这样的错误了。

    2.客户端存储方案 (cookie、localStorage、sessionStorage)

    接下来说一下认证信息在 客户端 存储的方式。首先明确,无论是明文用户名密码,还是 sessionId 和 token,都可以用三种方式存储,即 cookie、localStorage 和 sessionStorage。

    但 cookie 和 local/session Storage 分工又有所不同,cookie 可以作为传递的参数,并可通过后端进行控制,local/session Storage 则主要用于在客户端中保存数据,其传输需要借助 cookie 或其它方式完成。

    下面是三种方式的对比(参考文章 JS 详解 Cookie、 LocalStorage 与 SessionStorage

    特性cookielocalStoragesessionStorage
    生命周期一般由服务器生成,可设置失效时间。如果在浏览器端生成cookie,默认是关闭浏览器后失效除非被清除,否则永久保存仅在当前会话下有效,关闭页面或浏览器后被清除
    数据大小4K左右一般为5MB一般为5MB
    通信方式每次都会携带在HTTP头中,如果使用cookie保存过多数据会带来性能问题仅在客户端(即浏览器)中保存,不参与和服务器的通信同 localStorage

    通常来说,在可以使用 cookie 的场景下,作为验证用途进行传输的用户名密码、sessionId、token 直接放在 cookie 里即可。而后端传来的其它信息则可以根据需要放在 local/session Storage 中,作为全局变量之类进行处理。

    3.后端登录拦截

    终于可以粘代码了,但是在这之前还得再说两句。shiro 的安全管理实际上是基于会话实现的,所以我们没得选,用 session 方案就可以了。网上居然还有说 shiro + token 的,唉,这个问题槽点怎么这么多。。。

    上节课我们分析了 subject.login() 背后的故事,但还有一点没说,就是该过程会产生 session,并自动把 sessionId 设置到 cookie。这个看 DelegatingSubject 类还看不出来,还要再继续深入分析源码。我们简单做个测试,使用 postman 发一个登录请求:
    在这里插入图片描述
    可以看到响应头中第一行设置的 JSESSIONID ,即 sessionId 在 tomcat 中的叫法。

    好了,接下来我们来实现一下较为完善的访问拦截。上面说过,靠前端实现的拦截很容易被绕过。要想实现靠谱的拦截,必须由后端验证用户登录状态。这个思路并不难,就是前端带上 sesisonId 发送请求交由后端 认证,坑爹之处主要在于前后端分离的情况下需要额外的配置解决跨域问题。

    默认的情况下,跨域的 cookie 是被禁止的,后端不能设置,前端也不能发送,所以两边都要设置。

    首先编写一下拦截器 LoginInterceptor,主要是修改 preHandle 方法

        @Override
        public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
    
            // 放行 options 请求,否则无法让前端带上自定义的 header 信息,导致 sessionID 改变,shiro 验证失败
            if (HttpMethod.OPTIONS.toString().equals(httpServletRequest.getMethod())) {
                httpServletResponse.setStatus(HttpStatus.NO_CONTENT.value());
                return true;
            }
    
            Subject subject = SecurityUtils.getSubject();
            // 使用 shiro 验证
            if (!subject.isAuthenticated()) {
                return false;
            }
            return true;
        }
    

    由于跨域情况下会先发出一个 options 请求试探,这个请求是不带 cookie 信息的,所以 shiro 无法获取到 sessionId,将导致认证失败。这个地方坑了我好几个小时,要不是文章早就写完了,我什么都配置好了,请求发过来 sessionId 还是老变。也怪我一直盯着后端,没仔细看前端发的是啥请求。

    之后,为了允许跨域的 cookie,我们需要在配置类 MyWebConfigurer 做一些修改,主要是 addCorsMappings 方法:

        @Override
        public void addCorsMappings(CorsRegistry registry) {
            registry.addMapping("/**")
                    .allowCredentials(true)
                    .allowedOrigins("http://localhost:8080")
                    .allowedMethods("POST", "GET", "PUT", "OPTIONS", "DELETE")
                    .allowedHeaders("*")
        }
    

    这里注意,在 allowCredentials(true) ,即允许跨域使用 cookie 的情况下,allowedOrigins() 不能使用通配符 *,这也是出于安全上的考虑。

    4.前端配置

    为了让前端能够带上 cookie,我们需要通过 axios 主动开启 withCredentials 功能,即在 main.js 中添加一行

    axios.defaults.withCredentials = true
    

    这样,前端每次发送请求时就会带上 sessionId,shiro 就可以通过 sessionId 获取登录状态并执行是否登录的判断。

    现在还存在一个问题,即后端接口的拦截是实现了,但页面的拦截并没有实现,仍然可以通过伪造参数,绕过前端的路由限制,访问本来需要登录才能访问的页面。为了解决这个问题,我们可以修改 router.beforeEach 方法:

    router.beforeEach((to, from, next) => {
        if (to.meta.requireAuth) {
          if (store.state.user) {
            axios.get('/authentication').then(resp => {
              if (resp) next()
            })
          } else {
            next({
              path: 'login',
              query: {redirect: to.fullPath}
            })
          }
        } else {
          next()
        }
      }
    )
    

    即访问每个页面前都向后端发送一个请求,目的是经由拦截器验证服务器端的登录状态,防止上述情况的发生。后端这个接口可以暂时写成空的,比如:

        @ResponseBody
        @GetMapping(value = "api/authentication")
        public String authentication(){
            return "身份认证成功";
        }
    

    mark 一下这种思路,后来在权限控制中还要发挥重要作用。

    5.rememberMe

    上文提到 cookie 的生命周期如果未特别设置则与浏览器保持一致。我们看一下之前发送的登录请求的响应
    在这里插入图片描述
    JSESSIONID=C9D7C13C4C2444022AD865A23CA96189; Path=/; HttpOnly

    并没有设置存活时间,所以在关闭浏览器后,sessionId 就会消失,再次发送请求,shiro 就会认为用户已经变更。但有时我们需要保持登录状态,不然每次都要重新登录怪麻烦的,所以 shiro 提供了 rememberMe 机制。

    rememberMe 机制不是单纯地设置 cookie 存活时间,而是又单独保存了一种新的状态。之所以这样设计,也是出于安全性考虑,把 “记住我” 的状态与实际登录状态做出区分,这样,就可以控制用户在访问不太敏感的页面时无需重新登录,而访问类似于购物车、订单之类的页面时必须重新登录。

    为了启用 rememberMe,我们需要修改 shiro 配置类,添加两个方法:

        public CookieRememberMeManager rememberMeManager() {
            CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
            cookieRememberMeManager.setCookie(rememberMeCookie());
            cookieRememberMeManager.setCipherKey("EVANNIGHTLY_WAOU".getBytes());
            return cookieRememberMeManager;
        }
    
        @Bean
        public SimpleCookie rememberMeCookie() {
            SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
            simpleCookie.setMaxAge(259200);
            return simpleCookie;
        }
    

    之后,在登录方法中设置 UsernamePasswordTokenrememberMe 属性

    ···
    UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username, requestUser.getPassword());
    usernamePasswordToken.setRememberMe(true);
    try {
        subject.login(usernamePasswordToken);
        ···
        }
    

    这时再看登录方法的响应头,发现多了一条关于 rememberMe 的设置

    在这里插入图片描述

    里面有我们配置的存活时间 Max-Age=259200,单位是秒,259200 即 30 天。

    在拦截器中进行具体的判断逻辑,由于目前我们并没有特殊需求,所以姑且两种状态都放行:

     if (!subject.isAuthenticated() && !subject.isRemembered()) {
         return false;
     }
    

    可以通过

     System.out.println(subject.isRemembered());
     System.out.println(subject.isAuthenticated());
    

    测试一下,当正常登录时,控制台的输出为
    在这里插入图片描述
    关闭浏览器,直接访问需要登录的页面,仍然可以进入,但控制台的输出为
    在这里插入图片描述
    此外,可以通过在登录时设置单选框的方式,让用户自行决定是否启用记住我功能。

    下一步

    希望大家已经大致弄清楚 shiro 的作用以及如何在前后端之前传递认证相关的信息了。下一篇计划讲解根据用户权限动态渲染页面(菜单),又是一个大工程,我尽量在两周之内写完吧,再次感谢大家的支持与耐心等待。

    再多聊几句,凑个字数。近期有三个人问我怎么理财,他们分别是我的读者、同事和高中同学,我给的建议核心都是俩字:存钱。 你不要去相信网上那些铺天盖地的理财教程,什么复利是世界第八大奇迹,股市是年轻人的又一次跃升机会之类,要真这样他们哪还有功夫教你,都赶去跃升了。这些课程都是漏斗模型,层层筛选出最傻最好骗的韭菜,有点良心的无非是卖贵点的课给你,不要节操的会让你贷款上杠杆炒币炒空气炒到怀疑人生。

    理财是必要的,但它的意义在于 财务安全,即通过一定的知识让自身或者家庭不陷入由经济问题引起的危机。想要靠咱们手里的这点钱理财理出财务自由,难度堪比上小学的卢姥爷和 faker 打成五五开。人人都知道股市的二八定律,然而人人都以为自己是那能挣钱的两成人。可以说当你从没接触过股市却抱有这种期待的那一刻起,你就成为了一棵合格的韭菜。

    财务安全,一是要管控风险,即自己不了解的东西不碰,比如一个理财产品收益特别高,还号称没有风险,你想不明白为什么,就离它远远的。二是要未雨绸缪,为未来大概率出现的事情做准备,比如步入职场你就要考虑买房结婚买车生子教育父母养老。头一两年没心没肺玩一玩无所谓,大家都是这么过来的,但这些问题你越早面对,将来承担的压力就越小。其实本来大家自发地也都能学会存钱、稳稳过日子,奈何现在挥舞镰刀的人越来越多,能保证自己不被收割就算是大智慧喽。

    最后,如果大家觉得我的文章有所帮助,可以点赞关注收藏评论什么的走一波,这些数据都很重要,对我是一种实打实的鼓励。

    上一篇:Vue + Spring Boot 项目实战(十三):使用 Shiro 实现用户信息加密与登录认证

    下一篇:Vue + Spring Boot 项目实战(十五):动态加载后台菜单

    展开全文
  • 文章目录认证技术框架1.不需要认证技术1.1数据包特征信息1.2不需要认证配置思路1.3不需要认证效果显示2.IP/MAC地址绑定技术2.1工作原理2.2配置思路2.3 配置结果显示2.4总结(1)设备无法获取交换机的ARP表的排查思路...
  • Shiro用户认证和用户授权流程

    千次阅读 2018-05-16 14:24:01
    shiro是apache的一个开源框架,是一个权限管理的框架,实现 用户认证、用户授权。 spring中有spring security (原名Acegi),是一个权限框架,它和spring依赖过于紧密,没有shiro使用简单。 shiro不依赖于...
  • 前后端分离开发—用户认证(token)

    千次阅读 2021-11-22 14:55:20
    用户认证 1.在单体项目中如何实现用户认证? 在单体项目中如何保证受限资源在用户未登录的情况下不允许访问? 单体项目中,视图资源(页面)和接口(控制器)在一台服务器中,用户的多次请求都会基于同一个回话...
  • 统一用户认证和单点登录解决方案

    万次阅读 2018-08-22 09:13:36
    本文以某新闻单位多媒体数据库系统为例,提出建立企业用户认证中心,实现基于安全策略的统一用户管理、认证和单点登录,解决用户在同时使用多个应用系统时所遇到的重复登录问题。 随着信息技术和网络技术的迅猛发展...
  • Spring Security用户认证流程源码详解

    万次阅读 多人点赞 2018-04-21 18:52:55
    上篇文章讲解了Spring Security的基本原理以及如何自定义用户认证逻辑,这篇文章将在上篇的基础上解读Spring Security的源码更清楚的了解它的认证逻辑流程。 本篇文章主要围绕下面几个问题来深入源码: 用户认证...
  • 前后端分离之JWT用户认证

    千次阅读 2018-06-19 22:28:57
    转载__原文地址:http://lion1ou.win/2017/01/18/前后端分离之JWT用户认证 在前后端分离开发时为什么需要用户认证呢?原因是由于HTTP协定是不储存状态的(stateless),这意味着当我们透过帐号密码验证一个使用者时,...
  • Apache-NiFi授权与用户认证

    千次阅读 热门讨论 2017-05-14 11:24:39
    Apache-NiFi授权与用户认证
  • SpringSecurity用户认证流程

    千次阅读 2018-10-22 14:51:58
    Spring Security的源码...一、用户认证流程 提到Spring Security核心就是一系列的过滤器链,当一个请求来的时候,首先要通过过滤器链的校验,校验通过之后才会访问用户各种信息。  这里要说明的是在过滤器的最前端...
  • 这是我在做用户认证开发过程中看到一位大神写的文章,不过源地址已经失效了,希望有可能未来还能看到传送门。在此转载是不忍心这么好的文章绝版 我在 github 上找到了作者的源码,有需要的可以去下载...
  • mqtt用户认证,订阅多个主题

    千次阅读 2019-05-22 11:25:08
    关闭匿名认证 etc/emq.conf 配置启用匿名认证: ##Allow Anonymous authentication mqtt.allow_anonymous = false 用户名密码认证 etc/plugins/emq_auth_username.conf auth.user.1.username = admin auth.user.1....
  • es用户认证与鉴权入门配置

    千次阅读 2020-05-29 08:33:43
    xpack.security.enabled: true 一,开启x-pack权限认证 在elasticsearch.yml文件中配置 xpack.security.enabled: true xpack.security.transport.ssl.enabled: true 二,初始化密码 ./elasticsearch-setup-passwords...
  • laravel的用户认证系统-手动认证用户

    千次阅读 2017-08-18 10:34:18
    Laravel 中实现用户认证非常简单。实际上,几乎所有东西都已经为你配置好了。配置文件位于 config/auth.php,其中包含了用于调整认证服务行为的、标注好注释的选项配置。 在其核心代码中,Laravel 的认证组件由 ...
  • Django 用户认证(Auth)组件

    万次阅读 2020-09-09 10:17:36
    文章目录Django 用户认证(Auth)组件实例实例实例实例实例 Django 用户认证(Auth)组件 Django 用户认证(Auth)组件一般用在用户的登录注册上,用于判断当前的用户是否合法,并跳转到登陆成功或失败页面。 Django...
  • 统一用户认证中心

    万次阅读 2018-06-22 15:24:04
    解决思路:提出一个统一认证中心,对所有的登陆逻辑做统一处理,此服务可调用 不同的管理系统,如:操作员系统、终端用户系统、QQ开放平台竺,可复合调用组装,再将结果返回;实现架构图:说明:1. API Gateway(ora....
  • 自定义用户认证系统之 authenticate

    千次阅读 2019-04-25 21:07:21
    当一个 User 模型对象带有用户名和密码时,且需要有别于 Django 默认的认证功能,比如用...当调用django.contrib.auth.authenticate() 时 —— 如何登入一个用户 中所描述的 —— Django 会尝试所有的认证后台进行...
  • Laravel 用户认证体系详解

    万次阅读 2017-11-29 14:34:11
    简单来说,用户认证就是系统对用户提供的登录信息进行校验的过程。这一过程可以抽象为如下几个部分: 用户如何提供登录信息,如何表示? 系统如何校验登录信息? 系统如何维护登录成功后的认证信息? laravel...
  • su切换用户 认证失败

    千次阅读 2019-03-13 20:59:55
    用Ubuntu创建一个新用户,用su xxx切换的时候,显示认证失败。 原因: 自己创建新用户时没有设置密码,默认是没有密码的。在Linux中没有密码的用户是无法登录的,所以创建了用户以后如果需要用该用户登录,还...
  • nginx_http_auth_basic_module模块实现让访问着,只有输入正确的用户密码才允许访问web内容。web上的一些内容不想被其他人知道,但是又想让部分人看到。nginx的http auth模块以及Apache http auth都是很好的解决方案...
  • Laravel实现多用户认证功能实现详解

    千次阅读 2017-01-08 18:10:14
    Laravel 5.2新增多用户认证支持,即同时允许不同数据表用户(如前台用户、后台用户)登录认证。下面我们就来简单介绍多用户登录及注册功能实现。 1、生成认证脚手架 首先我们使用Laravel 5.2提供的认证...
  • django中的用户认证

    千次阅读 2017-08-12 17:32:19
    本文主要介绍django的用户创建、授权,以及token等。创建用户使用django-admin命令创建工程时,默认创建了django的用户管理信息。 命令行下:$ python manage.py createsuperuser --username=lanyang --email=...
  • Apache 用户认证配置,步骤。

    千次阅读 2018-03-27 09:38:27
    http自带的htpasswd命令就可以生成授权用户数据文件。[root@zhang httpd]# bin/htpasswd -c /usr/local/httpd/conf/passwd zs #创建一个张三的用户New password: #密码Re-type new password: #密码Adding password ...
  • 权限管理——用户认证和用户授权

    万次阅读 2016-07-16 21:42:10
    因为做了权限的项目经理,so,恶补一下一个权限框架:shiro。其实作为框架首要目标是易于使用和理解。... Apche Shiro就是一个强大而灵活的开源安全框架,它干净利落地处理身份认证,授权,企业会话管理和加密。说
  • 当我们刚刚安装完成postgresql后,直接通过cmd psql打开时,无论我们输什么密码都是错误的。 其实是因为默认他会使用系统的账户名来登陆,而pgsql中其实没有这个账户,我们应该改成 psql -U postgres ... ...
  • 用户登录时我们需要对用户密码进行校验,我们可以自己来编写逻辑,不过Spring Security框架给我提供了一套很强大安全框架,本文是一篇SpringSecurity入门级的文章,主要介绍其基本原理与认证流程以及使用。...
  • JAVA中使用LDAP进行用户认证

    千次阅读 2016-04-25 09:55:01
    我们使用了Sakai这一套开源软件来提供SAI课程的在线学习,由于Sakai的用户需要在LDAP上进行认证,因此需要把用户认证放到LDAP上来。在学习使用LDAP的过程中遇到了一些问题,现在总结一下: 1、管理连接的...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 621,908
精华内容 248,763
关键字:

用户认证

友情链接: shujucaiji.zip