6 Star 72 Fork 28

JustryDeng / notebook

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
[03]Spring Security账号密码认证 + 自定义鉴权(示例).md 19.52 KB
一键复制 编辑 原始数据 按行查看 历史

Spring Security账号密码认证 + 自定义鉴权(示例)


效果演示

说明:

  1. 张三属于普通用户,能访问一些普通的页面以及/user页。
  2. 李四属于数据库管理员,能访问一些普通的页面以及/user页、/dba页。
  3. 王五属于超级管理员,能访问一些普通的页面以及/user页、/dba页、/admin页。

演示

  • 普通用户张三:

    在这里插入图片描述

  • 数据库管理员李四:

    在这里插入图片描述

  • 超级管理员王五:

    在这里插入图片描述

相关代码(库表)

项目整体说明

在这里插入图片描述

注:上图中,只对相对关键的内容进行了简单说明;完整测试项目,可详见文末链接

相关库表说明

在这里插入图片描述

几个相对关键的类

提示 完整测试项目,可详见文末链接

  • 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 <font size = "20" color = "#3CAA3C"><a href="https://gitee.com/JustryDeng">JustryDeng</a></font> <img src="https://gitee.com/JustryDeng/shared-files/raw/master/JustryDeng/avatar.jpg" />
      * @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 <font size = "20" color = "#3CAA3C"><a href="https://gitee.com/JustryDeng">JustryDeng</a></font> <img src="https://gitee.com/JustryDeng/shared-files/raw/master/JustryDeng/avatar.jpg" />
      * @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 <font size = "20" color = "#3CAA3C"><a href="https://gitee.com/JustryDeng">JustryDeng</a></font> <img src="https://gitee.com/JustryDeng/shared-files/raw/master/JustryDeng/avatar.jpg" />
      * @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 <font size = "20" color = "#3CAA3C"><a href="https://gitee.com/JustryDeng">JustryDeng</a></font> <img src="https://gitee.com/JustryDeng/shared-files/raw/master/JustryDeng/avatar.jpg" />
      * @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 <font size = "20" color = "#3CAA3C"><a href="https://gitee.com/JustryDeng">JustryDeng</a></font> <img src="https://gitee.com/JustryDeng/shared-files/raw/master/JustryDeng/avatar.jpg" />
      * @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账号密码认证 + 自定义鉴权,简单示例完毕 !

相关资料

1
https://gitee.com/JustryDeng/notebook.git
git@gitee.com:JustryDeng/notebook.git
JustryDeng
notebook
notebook
master

搜索帮助