通过一和二已经完成了用户的认证和授权,但是用户权限和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){}
在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 Collectionauthorities = 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, CollectionconfigAttributes) 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修改SecurityConfig.java配置类的protected void configure(HttpSecurity http) 方法, 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; } }
@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")//实现最后的配置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 } }
工程目录如下:
2是自定义授权的就是从数据库获取url和url对应的权限,这里没有从数据库获取,但只要把MyInvocationSecurityMetadataSourceService.java类的 loadResourceDefine()方法改成从数据库获取就行了。
/**
* 加载权限表中所有权限
*/public void loadResourceDefine() {}
3是 csrf的防护,这里没有讲,可以把源码下载下来看一下。
4是异常处理的这里也没有讲。
sqlserver2008的jar在mave中找不到,需要自己手动添加到本地仓库。
运行结果:
源码地址:https://download.csdn.net/download/u014572215/10413520点击打开链接