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 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

  • 5
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值