美文网首页
Spring Security -- 安全

Spring Security -- 安全

作者: saoraozhe3hao | 来源:发表于2018-12-13 18:20 被阅读0次

    概念
    认证:即登录,authentication
    授权:即允许某种操作,authorization
    会话:即保持已登录状态
    RBAC:Role-Based Access Control ,基于角色的访问控制
    业务系统:即前台用户系统
    内管系统:即后台管理系统

    Spring Boot Security 应用组成

    1、初始化Spring Boot 应用
    2、在pom中增加依赖管理

        <dependencyManagement>
            <dependencies>
                <dependency>
                    <groupId>io.spring.platform</groupId>
                    <artifactId>platform-bom</artifactId> <!-- Spring Framework依赖管理,来自Spring IO Platform项目 -->
                    <version>Cairo-SR6</version>
                    <type>pom</type>
                    <scope>import</scope>
                </dependency>
                <dependency>
                    <groupId>org.springframework.cloud</groupId>
                    <artifactId>spring-cloud-dependencies</artifactId> <!-- Spring Cloud 依赖管理 -->
                    <version>Greenwich.M3</version>
                    <type>pom</type>
                    <scope>import</scope>
                </dependency>
            </dependencies>
        </dependencyManagement>
    

    加了依赖管理后,设置依赖不需要指明版本,且不需要以下配置:

        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
        </parent>
    

    3、配置Maven依赖

            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.security</groupId>
                <artifactId>spring-security-web</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.security</groupId>
                <artifactId>spring-security-config</artifactId>
            </dependency>
    

    4、从数据库中查询用户详情的实现类

    @Component
    public class MyUserDetailsService implements UserDetailsService {
        @Autowired
        private PasswordEncoder passwordEncoder;
    
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            // 根据用户名,从数据库找出用户信息
            // 参数依次为:用户名,数据库里记录的密码,可用,未过期,密码未过期,未被锁定,权限列表
            // Spring Security 会 自动调用 PasswordEncoder.match() 来判断密码是否正确
            return new User(username, passwordEncoder.encode("密码"), true, true,
                    true, true, AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_角色1,权限1"));
        }
    }
    

    5、登录成功 、 登录失败、登出成功 处理类

    @Component
    public class LoginSuccessHandler implements AuthenticationSuccessHandler {
        @Autowired
        private ObjectMapper objectMapper;
    
        public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
            response.setContentType("application/json;charset=UTF-8");
            // 返回一个 json
            response.getWriter().write(objectMapper.writeValueAsString(authentication)); // authentication 里有权限列表
        }
    }
    
    @Component
    public class LoginFailureHandler implements AuthenticationFailureHandler {
        @Autowired
        private ObjectMapper objectMapper;
    
        public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
            response.setContentType("application/json;charset=UTF-8");
            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
            // 返回一个 json
            response.getWriter().write(objectMapper.writeValueAsString(e)); // e 认证失败的原因
        }
    }
    
    @Component
    public class LogOutHandler implements LogoutSuccessHandler {
        @Autowired
        private ObjectMapper objectMapper;
    
        public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
            response.setContentType("application/json;charset=UTF-8");
            // 返回一个 json
            response.getWriter().write(objectMapper.writeValueAsString(authentication));
        }
    }
    

    6、Spring Security 配置类

    @Configuration
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
        @Autowired
        private LoginSuccessHandler loginSuccessHandler;
        @Autowired
        private LoginFailureHandler loginFailureHandler;
        @Autowired
        private LogOutHandler logOutHandler;
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.formLogin()      // 使用表单登录
                    .loginPage("/login.html")  // 未登录时重定向到登录页面, 不指定则使用Spring security 默认提供的登录页面
                    .loginProcessingUrl("/api/login")  // 指定 登录接口 url
                    .successHandler(loginSuccessHandler)  // 指定登录成功处理类,不指定则重定向
                    .failureHandler(loginFailureHandler) // 指定登录失败处理类,不指定则重定向
                    .and()
                    .authorizeRequests()   // 开始授权配置
                    .antMatchers("/*.html").permitAll()  // 对*.html 的请求,无需权限
                    .antMatchers(HttpMethod.POST, "/manage/*").hasRole("manager") // 对 /manage/* 的请求,需要拥有manager角色
                    .antMatchers("/client/*").hasAuthority("client") // 对 /client/* 的请求,需要拥有client权限
                    .anyRequest().authenticated()           // 针对所有请求,进行身份认证
                         
                    .and()
                    .logout()   // 开始 登出配置
                    .logoutUrl("/signOut")  // 登出接口,默认为 /logout
                    .logoutSuccessUrl("/login.html")  // 登出重定向到的路径,默认为loginPage
                    .logoutSuccessHandler(logOutHandler) // 与logoutSuccessUrl互斥
                    .deleteCookies("JSESSION") // 登出时 清理 cookie
                    .and()
                    .csrf()   // 开始csrf配置
                    .disable();  // 放开csrf防御
            super.configure(http);
        }
    
        @Bean
        public PasswordEncoder passwordEncoder() {
            // 这是Spring提供的一个密码加密器,加盐散列,并将盐拼入散列值,可用防止散列撞库
            return new BCryptPasswordEncoder(); // 也可以自己实现一个 PasswordEncoder
        }
    
        @Bean
        // 允许跨域配置
        public CorsConfigurationSource corsConfigurationSource() {
            final CorsConfiguration config = new CorsConfiguration();
            config.setAllowCredentials(true);
            config.addAllowedOrigin("*");
            config.addAllowedHeader("*");
            config.addAllowedMethod("*");
            final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
            source.registerCorsConfiguration("/**", config);
            return source;
        }
    }
    

    图片验证码验证

    1、配置Maven依赖

            <dependency>
                <groupId>commons-lang</groupId>
                <artifactId>commons-lang</artifactId>
            </dependency>
    

    2、认证异常类

    // AuthenticationException 是抽象类,不能实例化,因此需要自定义一个 验证码异常类
    public class ValidateCodeException extends AuthenticationException {
        public ValidateCodeException(String msg) {
            super(msg);
        }
    }
    

    3、验证码过滤器

    @Component
    public class ValidateCodeFilter extends OncePerRequestFilter {
    
        @Autowired
        private LoginFailureHandler loginFailureHandler;
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
            // 只过滤登录接口
            if (StringUtils.equals("/api/login", request.getRequestURI()) && StringUtils.equalsIgnoreCase("post", request.getMethod())) {
                try {
                    validate(new ServletWebRequest(request));
                }
                catch (ValidateCodeException e) {
                    loginFailureHandler.onAuthenticationFailure(request, response, e);
                    return;
                }
            }
            filterChain.doFilter(request, response);
        }
        private void validate(ServletWebRequest request) throws ServletRequestBindingException {
            // 这里从session中取出验证码值进行比对
            throw new ValidateCodeException("验证码不匹配");
        }
    }
    

    4、Spring Security配置类

    @Configuration
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
        @Autowired
        private ValidateCodeFilter validateCodeFilter;
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            // 在验证账号 之前 验证 图片验证码
            http.addFilterBefore( validateCodeFilter, UsernamePasswordAuthenticationFilter.class )
                    .formLogin();
            super.configure(http);
        }
    
        @Bean
        public PasswordEncoder passwordEncoder() {
            // 这是Spring提供的一个密码加密器,加盐散列,并将盐拼入散列值,可用防止散列撞库
            return new BCryptPasswordEncoder(); // 也可以自己实现一个 PasswordEncoder
        }
    }
    

    短信登录

    1、仿照 UsernamePasswordAuthenticationToken,定义Token类

    public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
    
        private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
    
        private final Object principal;
    
        public SmsCodeAuthenticationToken(String mobile) {
            super(null);
            this.principal = mobile;
            setAuthenticated(false);
        }
    
        public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
            super(authorities);
            this.principal = principal;
            super.setAuthenticated(true);
        }
    
    
        public Object getCredentials() {
            return null;
        }
    
        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();
        }
    }
    

    2、仿照 UsernamePasswordAuthenticationFilter,定义Filter类

    public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    
        private String mobileParameter = "mobile";    // 字段名
        private boolean postOnly = true;
    
        public SmsCodeAuthenticationFilter() {
            super(new AntPathRequestMatcher("/api/mobileLogin", "POST"));  // 短信登录接口
        }
    
        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);
            if (mobile == null) {
                mobile = "";
            }
            mobile = mobile.trim();
            SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);
            setDetails(request, authRequest);
    
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    
    
        protected String obtainMobile(HttpServletRequest request) {
            return request.getParameter(mobileParameter);
        }
    
    
        protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
            authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
        }
    
        public void setMobileParameter(String parameter) {
            this.mobileParameter = parameter;
        }
    
        public void setPostOnly(boolean postOnly) {
            this.postOnly = postOnly;
        }
    
        public final String getMobileParameter() {
            return mobileParameter;
        }
    }
    

    3、仿照 DaoAuthenticationProvider 实现 Provider类

    public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
    
        private UserDetailsService userDetailsService;
    
        public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    
            SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;
            
            UserDetails user = userDetailsService.loadUserByUsername((String) authenticationToken.getPrincipal());
    
            if (user == null) {
                throw new InternalAuthenticationServiceException("无法获取用户信息");
            }
            
            SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(user, user.getAuthorities());
            
            authenticationResult.setDetails(authenticationToken.getDetails());
    
            return authenticationResult;
        }
    
        public boolean supports(Class<?> authentication) {
            return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
        }
    
        public UserDetailsService getUserDetailsService() {
            return userDetailsService;
        }
    
        public void setUserDetailsService(UserDetailsService userDetailsService) {
            this.userDetailsService = userDetailsService;
        }
    }
    

    4、短信验证码过滤器

    // 与图片验证码过滤器 类似
    

    5、配置 Filter类 和 Provider类

    @Component
    public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
        
        @Autowired
        private AuthenticationSuccessHandler authenticationSuccessHandler;
        
        @Autowired
        private AuthenticationFailureHandler imoocAuthenticationFailureHandler;
        
        @Autowired
        private MyUserDetailsService userDetailsService;
        
        @Override
        public void configure(HttpSecurity http) throws Exception {
            
            SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
            smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
            smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
            smsCodeAuthenticationFilter.setAuthenticationFailureHandler(imoocAuthenticationFailureHandler);
            
            SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
            smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);
            
            http.authenticationProvider(smsCodeAuthenticationProvider)
                .addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
        }
    }
    

    6、Spring Security 配置

    @Configuration
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
        @Autowired
        private LoginSuccessHandler loginSuccessHandler;
        @Autowired
        private LoginFailureHandler loginFailureHandler;
        @Autowired
        private ValidateCodeFilter validateCodeFilter;
        @Autowired
        private SmsCodeAuthenticationSecurityConfig authenticationSecurityConfig;
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.addFilterBefore( validateCodeFilter, UsernamePasswordAuthenticationFilter.class )
                    .formLogin() 
                    .apply(authenticationSecurityConfig);
            super.configure(http);
        }
    }
    

    RBAC数据模型

    表:用户表,角色表,资源(权限)表,用户角色关系表,角色资源关系表
    资源表:存储权限控制目标,例如:菜单、按钮、URL

    最佳实践
    1、业务系统,一般权限控制比较简单,无需RBAC
    2、内管系统,需要RBAC,并且系统中有管理RBAC数据的界面
    3、资源表的值可以设置为 "对象.操作",例如 "order.delete"表示订单的删除权限,"order.delete"表示订单的删除权限,"coupon.all"表示优惠券的所有权限
    4、前端 根据 RBAC 数据 隐藏 入口(菜单,按钮)
    5、后端 根据 RBAC 数据表存储的 角色 与 可访问的 URL 控制访问

    Spring Security 整合 RBAC
    1、RBAC 权限判断 类

    // 案例 1
    @Component("rbacService")
    public class RbacService {
        private AntPathMatcher antPathMatcher = new AntPathMatcher();
    
        public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
            Object principal = authentication.getPrincipal();
            if(principal instanceof UserDetails){
                // 用户名
                String username = ((UserDetails)principal).getUsername();
                // 根据用户名 找到 当前用户可访问的 url 列表
                Set<String> urls = new HashSet<String>();
                for(String url : urls){
                    if(antPathMatcher.match(url, request.getRequestURI())){
                        return true;
                    }
                }
            }
            return false;
        }
    }
    
    // 案例 2
    // 权限的格式为 module.method,判断当前url是否包含在权限列表里
    @Component("rbacService")
    public class RbacService {
        private AntPathMatcher antPathMatcher = new AntPathMatcher();
    
        public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
            // 未登录
            if(authentication.getPrincipal() instanceof String){
                return false;
            }
            Collection<? extends GrantedAuthority> authorityList = authentication.getAuthorities();
            String method = request.getMethod().toLowerCase();
            String path = request.getServletPath();
            for (GrantedAuthority authority : authorityList) {
                // authority 的格式为 module.allow
                // allow取值: get、post、put、delete、all,默认为all
                String[] authoritySegment = authority.getAuthority().split("\\.");
                String module = authoritySegment[0];
                String allow = "all";
                if(authoritySegment.length > 1){
                    allow = authoritySegment[1];
                }
                String regexStart = "/admin/" + module + "/";
                String regexEnd = "/admin/" + module + "$";
                if (path.matches(regexStart) || path.matches(regexEnd) || module.equals("all")) {
                    // 权限列表里有的模块,才允许访问,且请求方法需要匹配
                    if (method.equals("get") || method.equals(allow) || allow.equals("all")) {
                        return true;
                    }
                }
            }
            return false;
        }
    }
    
    

    2、Spring Security 配置

            http.authorizeRequests()
                    .antMatchers("/*.html").permitAll()  // 对*.html 的请求,放开所有权限
                     // 进行权限判断,此外仍然会判断是否认真
                    .antMatchers("/manage").access("@rbacService.hasPermission(request, authentication)")   
                    .anyRequest().authenticated()      // anyRequest()必须放在authorizeRequests的最后,且只能有一个
            super.configure(http);
    

    相关文章

      网友评论

          本文标题:Spring Security -- 安全

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