spring security (三) 数据库认证,获取用户权限和url地址

通过一和二已经完成了用户的认证和授权,但是用户权限和url都是写在配置类SecurityConfig.java中的。这次我们把这些内容到放在数据库中,在系统启动时从数据库中获取。


配置自定义的用户服务

    spring security大体上是由一堆Filter(所以才能在spring mvc前拦截请求)实现的,Filter有几个,登出Filter(LogoutFilter),用户名密码验证Filter(UsernamePasswordAuthenticationFilter)之类的,Filter再交由其他组件完成细分的功能,例如最常用的UsernamePasswordAuthenticationFilter会持有一个AuthenticationManager引用,AuthenticationManager顾名思义,验证管理器,负责验证的,但AuthenticationManager本身并不做具体的验证工作,AuthenticationManager持有一个AuthenticationProvider集合,AuthenticationProvider才是做验证工作的组件,AuthenticationManager和AuthenticationProvider的工作机制可以大概看一下这两个的java doc,然后成功失败都有相对应该Handler 。大体的spring security的验证工作流程就是这样了

      假设我们需要认证的用户存储在非关系型数据库中,如Mongo或Neo4j,在这种情况下,我们需要提供一个自定义的UserDetailsService接口实现。

UserDetailsService接口非常简单:

public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

}

我们所需要做的就是实现loadUserByUsername()方法,根据给定的用户名来查找用户。loadUserByUsername()方法会返回代表给定用户的UserDetails对象。如下的程序清单展现了一个UserDetailsService的实现,它会从数据库中查找用户。

 @Override

public UserDetails loadUserByUsername(String username)throws UsernameNotFoundException,DataAccessException{

      //通过username从数据库获取用户名密码

      SystemUser authorUser=jdbcAuthorUser.findByUserName(username);

        if(authorUser!=null){
            //在这里获取权限,authorUser用户所应有的权限
            Listauthorities=new ArrayList();//创建权限列表
            authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
            authorities.add(new SimpleGrantedAuthority("ROLE_USER1"));
            return new User(authorUser.getUsername(),authorUser.getPassword(),authorities);//返回user
        }
        throw new UsernameNotFoundException("User '"+username+"' not found");

    }

在SecurityConfig.java配置类中做相应的配置:

 //创建DaoAuthenticationProvider认证的bean
    @Bean
    DaoAuthenticationProvider daoAuthenticationProvider() {
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        daoAuthenticationProvider.setPasswordEncoder(new BCryptPasswordEncoder());//加密用的
        daoAuthenticationProvider.setUserDetailsService(new CustomUserDetailsService(jdbcAuthorUser));
        return daoAuthenticationProvider;

    }

    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
        //会进行多种方式认证,当第一种不成功时会进行第二种认证
        ProviderManager authenticationManager = new ProviderManager(Arrays.asList(daoAuthenticationProvider(), inMemoryAuthenticationProvider));//多个认证方式
        //不擦除认证密码,擦除会导致TokenBasedRememberMeServices因为找不到Credentials再调用UserDetailsService而抛出UsernameNotFoundException
        authenticationManager.setEraseCredentialsAfterAuthentication(false);//验证后设置擦除凭证
        return authenticationManager;

    }

这里配置类两种认证方式。

inMemoryAuthenticationProvider是基于内存的认证,这里用户依然是从数据库中获取:

@Component
public class InMemoryAuthenticationProvider implements AuthenticationProvider {
    private final String adminName = "root";
    private final String adminPassword = "root";

    @Autowired
    private CustomUserDetailsService userDetailsService;
    //根用户拥有全部的权限
    private final Collection authorities = new ArrayList();

    //方法就是验证过程,如果AuthenticationProvider返回了null,AuthenticationManager会交给下一个支持authentication类型的AuthenticationProvider处理
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        //这里未进行密码加密
        UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication;
        UserDetails userDetails = userDetailsService.loadUserByUsername(token.getName());
        if (userDetails == null) {
            throw new UsernameNotFoundException("找不到该用户");
        }
        if (!userDetails.getPassword().equals(token.getCredentials().toString())) {
            throw new BadCredentialsException("密码错误");
        }
        return new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());
    }
    //support方法检查authentication的类型是不是这个AuthenticationProvider支持的
    @Override
    public boolean supports(Class authentication) {
        return UsernamePasswordAuthenticationToken.class.equals(authentication);
        //return true;
    }
}

support方法检查authentication的类型是不是这个AuthenticationProvider支持的,这里我简单地返回true,就是所有都支持,这里所说的authentication为什么会有多个类型,是因为多个AuthenticationProvider可以返回不同的Authentication。

授权

    Spring的决策管理器,其接口为AccessDecisionManager,抽象类为AbstractAccessDecisionManager。AccessDecisionManager实际上是由一个或多个决定是否访问的投票者的组合体。这个组合封装了允许/拒绝/放弃观看资源的用户逻辑。投票者决定结果是通过ACCESS_GRANTED , ACCESS_DENIED和ACCESS_ABSTAIN中的AccessDecisionVoter接口中定义的常量字段来表示。我们可以定义自定义访问决策,并注入到我们的访问决策管理器中。

Spring提供了3个决策管理器,至于这三个管理器是如何工作的请查看SpringSecurity源码
AffirmativeBased     一票通过,只要有一个投票器通过就允许访问
ConsensusBased     有一半以上投票器通过才允许访问资源

UnanimousBased    所有投票器都通过才允许访问

默认情况下, AffirmativeBased访问决策管理器将由两个投票者初始化:RoleVoter和AuthenticatedVoter 。如果用户具有访问资源的角色,RoleVoter授权访问,角色必须有“ ROLE_ ”前缀。

下面来实现一个简单的自定义决策管理器,这个决策管理器并没有使用投票器:

MyAccessDecisionManager.java类

@Service
public class MyAccessDecisionManager implements AccessDecisionManager {
    /**
     * 决策方法: 如果方法执行完毕没有抛出异常,则说明可以放行, 否则抛出异常 AccessDeniedException
     * @param authentication      认证过的票据Authentication,确定了谁正在访问资源
     * @param object              被访问的资源object
     * @param configAttributes    访问资源要求的权限配置ConfigAttributeDefinition
     * @throws AccessDeniedException
     * @throws InsufficientAuthenticationException
     */
    @Override
    public void decide(Authentication authentication, Object object, Collection configAttributes)
            throws AccessDeniedException, InsufficientAuthenticationException {
        if (null == configAttributes || configAttributes.size() <= 0) {
            return;
        }
        ConfigAttribute c;
        String needRole;
        for (Iterator iter = configAttributes.iterator(); iter.hasNext(); ) {
            c = iter.next();
            needRole = c.getAttribute();
            //authentication 为在注释1中循环添加到 GrantedAuthority 对象中的权限信息集合
            for (GrantedAuthority ga : authentication.getAuthorities()) {
                if (needRole.trim().equals(ga.getAuthority())) {
                    // 说明此URL地址符合权限,可以放行
                    return;
                }
            }
        }
        //没有权限
        throw new AccessDeniedException("no right");
    }

    @Override
    public boolean supports(ConfigAttribute attribute) {
        return true;
    }

    @Override
    public boolean supports(Class clazz) {
        //return FilterInvocation.class.isAssignableFrom(clazz);
        return true;
    }

}

decide这个方法没有任何的返回值,需要在没有通过授权时抛出AccessDeniedException。

自定义MyFilterSecurityInterceptor.java类

@Service
public class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
    @Autowired
    private FilterInvocationSecurityMetadataSource securityMetadataSource;

    @Autowired
    public void setMyAccessDecisionManager(MyAccessDecisionManager myAccessDecisionManager) {
        super.setAccessDecisionManager(myAccessDecisionManager);
    }
    public FilterInvocationSecurityMetadataSource getSecurityMetadataSource(){
        return this.securityMetadataSource;
    }

    @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);
        invoke(fi);
    }

    public void invoke(FilterInvocation fi) throws IOException, ServletException {
         //fi里面有一个被拦截的url
         //里面调用MyInvocationSecurityMetadataSource的getAttributes(Object object)这个方法获取fi对应的所有权限
         //再调用MyAccessDecisionManager的decide方法来校验用户的权限是否足够
        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.securityMetadataSource;
    }
}
MyInvocationSecurityMetadataSourceService.java 类url权限类,判定用户请求的url 是否在权限表中,如果在权限表中,则返回给 decide 方法,用来判定用户是否有此权限。如果不在权限表中则放行。

@Service
public class MyInvocationSecurityMetadataSourceService implements FilterInvocationSecurityMetadataSource {

    private HashMap, Collection> map = null;

    /**
     * 加载权限表中所有权限,这里不想从数据库中获取直接写在了这
     */
    public void loadResourceDefine() {
        map = new HashMap<>();
        Collection array;
        ConfigAttribute cfg, cfg1;
        array = new ArrayList<>();
        cfg = new SecurityConfig("ROLE_USER");
        cfg1 = new SecurityConfig("ROLE_USER1");
        array.add(cfg);
        array.add(cfg1);
        map.put("/test/index", array);
        //map.put("/test/index", array);
    }

    //此方法是为了判定用户请求的url 是否在权限表中,如果在权限表中,则返回给 decide 方法,
    // 用来判定用户是否有此权限。如果不在权限表中则放行。
    @Override
    public Collection getAttributes(Object object) throws IllegalArgumentException {
        System.out.println("object的类型为:" + object.getClass());
        FilterInvocation filterInvocation = (FilterInvocation) object;
        String url = filterInvocation.getRequestUrl();
        System.out.println("访问的URL地址为(包括参数):" + url);
        url = filterInvocation.getRequest().getServletPath();
        System.out.println("访问的URL地址为:" + url);
        if (map == null)
            loadResourceDefine();
        //object 中包含用户请求的request 信息
        final HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
        AntPathRequestMatcher matcher;
        String resUrl;
        for (Iterator iter = map.keySet().iterator(); iter.hasNext(); ) {
            resUrl = iter.next();
            matcher = new AntPathRequestMatcher(resUrl);
            //matches() 方法用于检测字符串是否匹配给定的正则表达式
            boolean a=matcher.matches(request);
            if (matcher.matches(request)) {
                Collection c = map.get(resUrl);
                return map.get(resUrl);
            }
        }
        return null;
        //return collection;
    }

    @Override
    public Collection getAllConfigAttributes() {
        return null;
    }

    @Override
    public boolean supports(Class clazz) {
        //UsernamePasswordAuthenticationToken.class.equals(clazz);
        return FilterInvocation.class.isAssignableFrom(clazz);
        //return true;
    }
}
修改SecurityConfig.java配置类的protected void configure(HttpSecurity http) 方法

  @Override
    protected void configure(HttpSecurity http) throws Exception {
        CsrfTokenResponseHeaderBindingFilter csrfTokenFilter = new CsrfTokenResponseHeaderBindingFilter();
        CustomAccessDeniedHandler accessDeniedHandler = new CustomAccessDeniedHandler();
        http.csrf().disable()//关闭CSRF
               UsernamePasswordAuthenticationFilter.class)
                .exceptionHandling()
                .authenticationEntryPoint(unauthorizedEntryPoint)
                .and()
                .formLogin()
                .loginPage("/login").loginProcessingUrl("/login.do")
                .failureUrl("/lError?error")
                .usernameParameter("username")
                .passwordParameter("password")
                .successHandler(ajaxAuthSuccessHandler)
                .failureHandler(ajaxAuthFailHandler)
                .defaultSuccessUrl("/welcome").permitAll()
                .and()
                .logout()
                .logoutUrl("/logout")
                .logoutSuccessUrl("/home/logout").permitAll()
                .and()
                .rememberMe()

                .tokenRepository(tokenRepository())

    //.userDetailsService(new CustomUserDetailsService(jdbcAuthorUser))

                .tokenValiditySeconds(2100000).key("loginKey")//实现
                .and()
                .authorizeRequests()
                .anyRequest().authenticated()
                /*.antMatchers(HttpMethod.POST, "/spittles").authenticated()
                .antMatchers("/test/index").hasAnyAuthority("ROLE_USER")
                .anyRequest().permitAll()*/
                .and()
                .sessionManagement()//配置session管理
                .maximumSessions(1)
                .expiredUrl("/login");//其他请求不需要认证
        http.addFilterBefore(myFilterSecurityInterceptor, FilterSecurityInterceptor.class)
                //.addFilterBefore(JdbcTokenRepositoryImpl,JdbcTokenRepositoryImpl.class)
                .addFilterAfter(csrfTokenFilter, CsrfFilter.class).exceptionHandling()
                .accessDeniedHandler(accessDeniedHandler);
        //.accessDeniedPage("/lError?error")//异常市跳转到页面;
        // 由这个决定使用那个FilterSecurityInterceptor
    }

最后的配置SecurityConfig.java类如下

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    JdbcAuthorUser jdbcAuthorUser;
    @Autowired
    InMemoryAuthenticationProvider inMemoryAuthenticationProvider;
    @Autowired
    private MyFilterSecurityInterceptor myFilterSecurityInterceptor;

    //以下三个是ajax登录请求的配置
    @Autowired
    UnauthorizedEntryPoint unauthorizedEntryPoint;
    @Autowired
    AjaxAuthFailHandler ajaxAuthFailHandler;
    @Autowired
    AjaxAuthSuccessHandler ajaxAuthSuccessHandler;


    //创建DaoAuthenticationProvider认证的bean
    @Bean
    DaoAuthenticationProvider daoAuthenticationProvider() {
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        daoAuthenticationProvider.setPasswordEncoder(new BCryptPasswordEncoder());//加密用的
        daoAuthenticationProvider.setUserDetailsService(new CustomUserDetailsService(jdbcAuthorUser));
        return daoAuthenticationProvider;
    }

    /**
     * 认证
     *
     * @return
     * @throws Exception
     */
    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
        //会进行多种方式认证,当第一种不成功时会进行第二种认证
        ProviderManager authenticationManager = new ProviderManager(Arrays.asList(daoAuthenticationProvider(), inMemoryAuthenticationProvider));//多个认证方式
        //不擦除认证密码,擦除会导致TokenBasedRememberMeServices因为找不到Credentials再调用UserDetailsService而抛出UsernameNotFoundException
        authenticationManager.setEraseCredentialsAfterAuthentication(false);//验证后设置擦除凭证
        return authenticationManager;
    }


  /*  //使用基于内存的认证,spring 实战第九章
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //暂时使用基于内存的AuthenticationProvider
        auth.inMemoryAuthentication().withUser("user").password("pass").roles("USER").and()
                .withUser("admin").password("pass").authorities("ROLE_USER", "ROLE_ADMIN");
    }*/

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/resources/**");
    }

    /**
     * Spring Security 4.0之后,引入了CSRF,默认是开启。不得不说,CSRF和RESTful技术有冲突。
     * CSRF默认支持的方法: GET|HEAD|TRACE|OPTIONS,不支持POST。
     * authenticated()  要求在执行该请求时必须已经登录了应用,未登录会重定向到登录页面。
     * permitAll()    允许请求没有任何的安全限制。
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        CsrfTokenResponseHeaderBindingFilter csrfTokenFilter = new CsrfTokenResponseHeaderBindingFilter();
        CustomAccessDeniedHandler accessDeniedHandler = new CustomAccessDeniedHandler();
        http.csrf().disable()//关闭CSRF
                .exceptionHandling()
                .authenticationEntryPoint(unauthorizedEntryPoint)
                .and()
                .formLogin()
                .loginPage("/login").loginProcessingUrl("/login.do")
                .failureUrl("/lError?error")
                .usernameParameter("username")
                .passwordParameter("password")
                .successHandler(ajaxAuthSuccessHandler)
                .failureHandler(ajaxAuthFailHandler)
                .defaultSuccessUrl("/welcome").permitAll()
                .and()
                .logout()
                .logoutUrl("/logout")
                .logoutSuccessUrl("/home/logout").permitAll()
                .and()
                .authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .sessionManagement()//配置session管理
                .maximumSessions(1)
                .expiredUrl("/login");//其他请求不需要认证
        http.addFilterBefore(myFilterSecurityInterceptor, FilterSecurityInterceptor.class)
                //.addFilterBefore(JdbcTokenRepositoryImpl,JdbcTokenRepositoryImpl.class)
                .addFilterAfter(csrfTokenFilter, CsrfFilter.class).exceptionHandling()
                .accessDeniedHandler(accessDeniedHandler);
        // 由这个决定使用那个FilterSecurityInterceptor

    }

}

工程目录如下:

spring security (三) 数据库认证,获取用户权限和url地址_第1张图片

2是自定义授权的就是从数据库获取url和url对应的权限,这里没有从数据库获取,但只要把MyInvocationSecurityMetadataSourceService.java类的 loadResourceDefine()方法改成从数据库获取就行了。

/**

     * 加载权限表中所有权限

     */

    public void loadResourceDefine() {}

3是 csrf的防护,这里没有讲,可以把源码下载下来看一下。

4是异常处理的这里也没有讲。

sqlserver2008的jar在mave中找不到,需要自己手动添加到本地仓库。

运行结果:

spring security (三) 数据库认证,获取用户权限和url地址_第2张图片

源码地址:https://download.csdn.net/download/u014572215/10413520点击打开链接

你可能感兴趣的:(Java,Web,sprng,security)