Spring Security -- 安全

2018-12-13  本文已影响0人  saoraozhe3hao

概念
认证:即登录,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);
上一篇 下一篇

猜你喜欢

热点阅读