声明: 本文先演示效果,然后再给出几个相对关键的类;完整测试项目,可详见文末链接
。
效果演示:
说明:
- 张三属于普通用户,能访问一些普通的页面以及/user页。
- 李四属于数据库管理员,能访问一些普通的页面以及/user页、/dba页。
- 王五属于超级管理员,能访问一些普通的页面以及/user页、/dba页、/admin页。
演示:
-
普通用户张三:
-
数据库管理员李四:
-
超级管理员王五:
相关代码(库表):
项目整体说明:![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/652ceb3918c4320a0769f19d2318d1a5.png#pic_center)
注:上图中,只对相对关键的内容进行了简单说明;完整测试项目,可详见文末链接
。
相关库表说明:
几个相对关键的类:
提示: 完整测试项目,可详见文末链接
。
-
MyAccessDecisionManager:
import com.pingan.springsecurity.model.MyUserDetails; import com.pingan.springsecurity.service.impl.MyUserDetailsService; import org.springframework.security.access.AccessDecisionManager; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; import org.springframework.security.authentication.InsufficientAuthenticationException; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.web.FilterInvocation; import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; import java.util.Collection; import java.util.List; import java.util.stream.Collectors; /** * 决策器 (判断鉴权是否通过) * * @author JustryDeng * @date 2019/12/14 11:34 */ @Component public class MyAccessDecisionManager implements AccessDecisionManager { /** * 决策 * * @param authentication * 当前用户的信息模型 * 注: 由于重写了{@link MyUserDetailsService#loadUserByUsername}, * 重写后该方法实际返回的类型是{@link MyUserDetails}, 所以这里直接 * 将Authentication强转为MyUserDetails。 * @param object * 当前request的封装 * @param configAttributes * 与访问的目标uri相关联的配置属性 * 注:在本示例中,不需要用到此属性; 如果在这里需要用到此属性的话,可以在通过 * 实现{@link FilterInvocationSecurityMetadataSource},重写相关返 * 回Collection<ConfigAttribute>的方法,该返回值会作为形参传递到本方法 * 然后在这里就能拿到对应的值了。 * 可参考网友的示例<linked>https://www.jianshu.com/p/e715cc993bf0</linked> * * @throws AccessDeniedException * 当前用户无权访问 * @throws InsufficientAuthenticationException * 当前用户信任级别不够,无法访问 * @date 2019/12/14 12:06 */ @Override public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException { HttpServletRequest request = ((FilterInvocation) object).getHttpRequest(); String targetPath = request.getRequestURI(); Object principal = authentication.getPrincipal(); /* * 若认证过,那么principal instanceof MyUserDetails为true, 否则用户没有认证过 * * 注: 鉴权一般都在认证之后, 没有认证谈何鉴权。 * * 注: 一旦抛出异常后, * 在{@link ExceptionTranslationFilter#doFilter}中会对抛出的AuthenticationException异常(包括其子异常) * 进行相关处理。如: 抛出AuthenticationCredentialsNotFoundException异常,就会被处理然后页面跳转至登录页 * */ if (!(principal instanceof MyUserDetails)) { throw new AuthenticationCredentialsNotFoundException(" there is no any Authentication object MyUserDetails in the SecurityContext"); } MyUserDetails myUserDetails = (MyUserDetails)principal; // 这个用户可访问的所有资源信息 Collection<? extends GrantedAuthority> grantedAuthority = myUserDetails.getAuthorities(); List<String> list = grantedAuthority.parallelStream() .map(GrantedAuthority::getAuthority) .collect(Collectors.toList()); if(list.contains(targetPath)) { // 鉴权通过, 有访问权限 return; } // 鉴权不通过, 没有访问权限 throw new AccessDeniedException( String.format("You(%s) don't have any authorizion access %s", myUserDetails.getName(), targetPath) ); } /** * 当前AccessDecisionManager实例能否处理 传递的ConfigAttribute呈现的授权请求 */ @Override public boolean supports(ConfigAttribute attribute) { return true; } /** * 当前AccessDecisionManager实例是否支持提供访问控制决策 */ @Override public boolean supports(Class<?> clazz) { return true; } }
-
MyFilterInvocationSecurityMetadataSource:
import com.pingan.springsecurity.mapper.DaoMapper; import com.pingan.springsecurity.model.ApiResource; import lombok.RequiredArgsConstructor; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.web.FilterInvocation; import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import javax.servlet.http.HttpServletRequest; import java.util.ArrayList; import java.util.Collection; import java.util.List; /** * 存储ConfigAttribute信息, 并根据ConfigAttribute信息的有无, 决定是否走 决策器 * 即: 若{@link this#getAttributes}返回的集合满足CollectionUtils.isEmpty(list)为true的话, * 那么不会走决策器, * 否者会走决策器 * * @author JustryDeng * @date 2019/12/14 11:20 */ @Component @RequiredArgsConstructor public class MyFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource { private List<String> needAuthPaths = new ArrayList<>(16); private final DaoMapper mapper; @PostConstruct private void init() { needAuthPaths.addAll(mapper.selectNeedAuthPaths()); } @Override public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException { HttpServletRequest request = ((FilterInvocation) object).getHttpRequest(); String targetPath = request.getRequestURI(); if (needAuthPaths.contains(targetPath)) { // 需要鉴权 List<ConfigAttribute> list = new ArrayList<>(1); list.add(ApiResource.builder().build()); return list; } // 不需要鉴权 return null; } @Override public Collection<ConfigAttribute> getAllConfigAttributes() { return null; } @Override public boolean supports(Class<?> clazz) { return true; } }
-
MySecurityInterceptor:
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.stereotype.Component; import javax.servlet.*; import java.io.IOException; /** * 通过MySecurityInterceptor 注册 MyAccessDecisionManager * * @author JustryDeng * @date 2019/12/14 12:50 */ @Component public class MySecurityInterceptor extends AbstractSecurityInterceptor implements Filter { private final MyFilterInvocationSecurityMetadataSource myFilterInvocationSecurityMetadataSource; public MySecurityInterceptor(MyAccessDecisionManager myAccessDecisionManager, MyFilterInvocationSecurityMetadataSource myFilterInvocationSecurityMetadataSource) { this.myFilterInvocationSecurityMetadataSource = myFilterInvocationSecurityMetadataSource; // 设置 以 自定义的决策权 进行 鉴权管理 super.setAccessDecisionManager(myAccessDecisionManager); } @Override public Class<?> getSecureObjectClass() { return FilterInvocation.class; } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { FilterInvocation fi = new FilterInvocation(servletRequest, servletResponse, filterChain); invoke(fi); } public void invoke(FilterInvocation fi) throws IOException, ServletException { InterceptorStatusToken token = super.beforeInvocation(fi); try { fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } finally { super.afterInvocation(token, null); } } /** * 返回自定义的SecurityMetadataSource */ @Override public SecurityMetadataSource obtainSecurityMetadataSource() { // 如果有实现SecurityMetadataSource的话,可以设置采用自定义的 资源器, 如: // 如果实现有FilterInvocationSecurityMetadataSource的话,可以将其进行注册(即:返回其实例) return myFilterInvocationSecurityMetadataSource; } }
-
MyWebSecurityConfigurerAdapter:
import com.pingan.springsecurity.service.impl.MyUserDetailsService; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; 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.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.password.PasswordEncoder; /** * SpringSecurity配置 * * @author JustryDeng * @date 2019/12/7 14:08 */ @Configuration @RequiredArgsConstructor public class MyWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter { private final MyUserDetailsService myUserDetailsService; @Override public void configure(WebSecurity web) { /* * 对于那些没必要进行保护的资源, 可以使用ignoring,使其跳过SpringSecurity * * 注:configure(HttpSecurity http)方法里的permitAll();也有类似的效果, * 不过permitAll会走SpringSecurity,只是说无条件放行而已。 */ web.ignoring().antMatchers("/picture/**"); web.ignoring().antMatchers("/md/**"); // 开发时,可以将SpringSecurity的debug打开 web.debug(false); } /** * SpringSecurity提供有一些基本的页面(如:login、logout等);如果觉得它提供的 * 基础页面难看,想使用自己的页面的话,可以在此方法里面进行相关配置。 */ @Override protected void configure(HttpSecurity http) throws Exception { // 设置登录方式为 表单登录 http.formLogin(); /// 设置登录方式为 弹框登录 /// http.httpBasic(); /// 自定义登录页 /// http.formLogin().loginPage("myLoginPae"); /// 自定义登出页 /// http.logout().logoutUrl("myLogoutPae"); // 登出成功时,跳转至此url http.logout().logoutSuccessUrl("/logout/success"); // 登录成功时,跳转至此url // 注意:如果未登录,直接访问 登录失败页的话,会被DefaultLoginPageGeneratingFilter识别,并跳转至登录页进行登录 http.formLogin().successForwardUrl("/index"); // 登录失败时,跳转至此url // 注意:如果未登录,直接访问 登录失败页的话,会被DefaultLoginPageGeneratingFilter识别,并跳转至登录页进行登录 http.formLogin().failureUrl("/login/failed"); /// 当鉴权不通过,是 跳转至此url http.exceptionHandling().accessDeniedPage("/403"); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // 配置 UserDetailsService, 用户自定义查询用户的信息 auth.userDetailsService(myUserDetailsService); } /** * 自定义 加密器 * * 注:只需要将其注册进入容器中即可,InitializeUserDetailsBeanManagerConfigurer类会从容器 * 拿去PasswordEncoder.class实现,作为其加密器 * * @date 2019/12/21 17:59 */ @Bean public PasswordEncoder myPasswordEncoder() { return new PasswordEncoder() { @Override public String encode(CharSequence rawPassword) { return rawPassword == null ? "" : rawPassword.toString(); } @Override public boolean matches(CharSequence rawPassword, String encodedPassword) { if (rawPassword == null || rawPassword.length() == 0) { return false; } return rawPassword.equals(encodedPassword); } }; } }
-
MyUserDetailsService:
import com.pingan.springsecurity.mapper.DaoMapper; import com.pingan.springsecurity.model.ApiResource; import com.pingan.springsecurity.model.MyUserDetails; import com.pingan.springsecurity.model.Role; import lombok.RequiredArgsConstructor; 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.Service; import java.util.List; import java.util.stream.Collectors; /** * 对{@link UserDetailsService#loadUserByUsername(String)}进行重写 * * @author JustryDeng * @date 2019/12/14 10:20 */ @Service @RequiredArgsConstructor public class MyUserDetailsService implements UserDetailsService { private final DaoMapper mapper; /** * 根据账号名, 查询用户信息 * * todo 用户名不存在时,不处理的话,最终抛出InternalAuthenticationServiceException */ @Override public UserDetails loadUserByUsername(String accountNo) throws UsernameNotFoundException { // 查询用户基本信息 MyUserDetails myUserDetails = mapper.selectUserBasicInfoByAccountNo(accountNo); // 查询用户角色信息 List<Role> roleList = mapper.selectRolesByUserId(myUserDetails.getId()); // 查询用户权限信息(即:查询用户可访问的资源) List<Integer> roleIdList = roleList.parallelStream().map(Role::getId).collect(Collectors.toList()); List<ApiResource> apiResources = mapper.selectApiResourcesByRoleIds(roleIdList); // 组装信息并返回 myUserDetails.setRoles(roleList); myUserDetails.setAccessibleApis(apiResources); return myUserDetails; } }
补充
轻量级自定义鉴权
这里只作简单提示不展开
通过SpEL指定调用自定义类实现自定义鉴权。即:在继承WebSecurityConfigurerAdapter作相关配置时,指定access调用自定义的方法即可
@Override
protected void configure(HttpSecurity http) throws Exception {
// 通过SPEL调用自定义的spring bean的方法即可, 该spring bean中应该有对应的方法,如:public boolean hasPermission(HttpServletRequest request, Authentication authentication)
http.authorizeRequests()
.antMatchers("/**").access("@mySpringBean.hasPermission(request, authentication)");
}
Spring Security账号密码认证 + 自定义鉴权,简单示例完毕 !
^_^ 如有不当之处,欢迎指正
^_^ 参考资料
《SpringSecurity5.2.1RELEASE源码》
^_^ 测试代码托管链接
https://github.com/JustryDeng…SpringSecurity…
^_^ 本文已经被收录进《程序员成长笔记》 ,笔者JustryDeng