SpringBoot + SpringSecurity 整合(1)

版本:SprintBoost2.7.0、  SpringSecurity5.4.x以上

众所周知,SpringSecurity是内部封装了登陆接口的。对于登入登出我们都不需要自己编写Controller接口,Spring Security为我们封装好了。默认登入路径:/login,登出路径:/logout。当然我们可以也修改默认的名字。

这里用法的采用的是上述方式,下面就让我们按流程来吧。

第一步,先引入jar



   org.springframework.boot
   spring-boot-starter-security
   2.7.0

然后,我们需要实现一个接口 UserDetails,这个对象将在登陆后,存储登陆的用户信息。(SysUser是我数据库中用户的实体,有常规的字段 username、password、nickname等...)

import com.google.common.collect.Lists;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

public class SysUserDetails extends SysUser implements UserDetails {

    /**
     * 返回授予用户的权限,能返回null。
     * 鉴权的时候会调用这个方法
     * @return
     */
    @Override
    public Collection getAuthorities() {
        //这里是捏造一个数据,固定登陆用户权限只有hello,后面测试权限使用
        SimpleGrantedAuthority hello = new SimpleGrantedAuthority("hello");
        return Lists.newArrayList(hello); //可以理解为 Arrays.asList(hello)
    }

    /**
     * 指示用户的帐户是否已过期。过期的帐户无法进行身份验证。
     * 返回:如果用户的帐户有效(即未过期),返回true,如果不再有效(即过期),返回false。
     * @return
     */
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    /**
     * 用户被锁定或解锁状态。被锁定的用户无法进行认证。
     * 如果用户没有被锁定,返回true,否则返回false
     * @return
     */
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    /**
     * 指示用户的凭据(密码)是否已过期。过期的凭据将阻止身份验证。
     * 返回:如果用户的凭证有效(即未过期),返回true,如果不再有效(即过期),返回false。
     * @return
     */
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    /**
     * 表示该用户是启用还是禁用。 被禁用的用户无法进行认证。
     * 返回:如果用户已启用,则为true,否则为false
     * @return
     */
    @Override
    public boolean isEnabled() {
        return true;
    }

有了实体对象,接下来是对应的service,实现的依旧是security的接口, 这个service的作用就是会在登陆校验用户名的时候进行数据查询。

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;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    //Security校验用户名和密码的时候,会调用查询数据库中的数据
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        SysUserDetails sysUserDetails = new SysUserDetails();
        sysUserDetails.setUsername("admin");
        sysUserDetails.setPassword("123456");
        //TODO 根据用户名查询用户,我这里只是个例子,就不查询了,写死一个用户名和密码。
        //完成校验,赋予授权
        return sysUserDetails;
    }
}

接下来需要一个查询权限的方法的类AuthDynamicSecurityServiceImpl ,这个接口是自定义的service接口


import org.springframework.security.access.ConfigAttribute;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;


@Service
public class AuthDynamicSecurityServiceImpl implements IDynamicSecurityService{

    /**
     * 查询权限
     * @return
     */
    @Override
    public Map loadDataSource() {
        Map map = new ConcurrentHashMap<>();
        //获取权限,将权限充进resourceList,这里我没有查询实体,所以用map来塞入一些固定的数据
//      List resourceList = adminService.getResourceList();
        List> resourceList = new ArrayList<>();
        Map resourceList1 = new HashMap<>();
        resourceList1.put("url", "/page1");
        resourceList1.put("perm",  "page1");
        resourceList.add(resourceList1);
        Map resourceList2 = new HashMap<>();
        resourceList2.put("url", "/page2");
        resourceList2.put("perm",  "page2");
        resourceList.add(resourceList2);
        resourceList.forEach((item -> {
            //这里返回security要求的权限类。
            map.put(item.get("url"), new org.springframework.security.access.SecurityConfig(item.get("perm")));
        }));
        return map;
    }
}

接下来我们需要一个存有所有权限的数据源DynamicSecurityMetadataSource ,每次接口权限校验的时候都会调用其校验权限的方法。

import cn.hutool.core.util.URLUtil;
import com.google.common.collect.Lists;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;

/**
 * @author lvshiyu
 * @description: 动态权限数据源,用于获取动态权限
 * @date 2022年07月05日 17:51
 */
@Component
public class DynamicSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

    /**
     * 静态权限集合,避免每次调接口都查询一次。数据形态 key为权限的url
     */
    private static Map configAttributeMap = null;

    /**
     * 查询权限的服务,这个服务需要把所有的权限查询出来存储
     */
    @Resource
    private IDynamicSecurityService dynamicSecurityService;

    /**
     * 初始化所有的对应权限集合
     */
    @PostConstruct
    public void loadDataSource() {
        //加载所有的URL和资源map
        configAttributeMap = dynamicSecurityService.loadDataSource();
    }

    /**
     * 获取访问路径所需要的权限
     * @param object
     * @return
     * @throws IllegalArgumentException
     */
    @Override
    public Collection getAttributes(Object object) throws IllegalArgumentException {
        //如果内存中的缓存数据为空,则加载所有的URL和资源map
        if (configAttributeMap == null) {
            this.loadDataSource();
        }
        List configAttributes = new ArrayList<>();
        //获取当前访问的路径
        String url = ((FilterInvocation) object).getRequestUrl();
        String path = URLUtil.getPath(url);
        //获取访问该路径所需资源
        PathMatcher pathMatcher = new AntPathMatcher();
        configAttributeMap.forEach((key, value) -> {
            if (pathMatcher.match(key, path)){
                configAttributes.add(value);
            }
        });
        //未设置操作请求权限,返回空集合
        return configAttributes;
    }

    /**
     * 清除权限集合,方便后期如果需要刷新的时候使用
     */
    public void clearDataSource() {
        configAttributeMap = null;
    }

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

    @Override
    public boolean supports(Class clazz) {
        return true;
    }

}

然后我们还需要一个决策器类DynamicAccessDecisionManager ,来取全部权限和用户权限进行比较


import cn.hutool.core.collection.CollUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;

import java.util.Collection;

@Component
@Slf4j
public class DynamicAccessDecisionManager implements AccessDecisionManager {

    /**
     * 访问权限决策
     * @param authentication 用户拥有的权限
     * @param object
     * @param configAttributes 资源所需要的权限
     * @throws AccessDeniedException
     * @throws InsufficientAuthenticationException
     */
    @Override
    public void decide(Authentication authentication, Object object, Collection configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
        //当接口未被配置资源时直接放行
        if (CollUtil.isEmpty(configAttributes)) {
            //未配置资源访问限制
            log.info("未配置资源访问限制");
            return;
        }
        for (ConfigAttribute attribute : configAttributes) {
            SimpleGrantedAuthority needAuthority = new SimpleGrantedAuthority(attribute.getAttribute());
            //将访问所需资源或用户拥有资源进行比对
            if (authentication.getAuthorities().contains(needAuthority)) {
                return;
            }
        }
        throw new AccessDeniedException("抱歉,您没有访问权限");
    }

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

    @Override
    public boolean supports(Class clazz) {
        return true;
    }
}

接下来还需要一个过滤器DynamicSecurityFilter,注意,这里的过滤器的两个set方法注入了一个是权限决策器,一个是全局权限数据源 。


import com.google.common.collect.Lists;
import org.springframework.http.HttpMethod;
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 org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.List;

@Component
public class DynamicSecurityFilter extends AbstractSecurityInterceptor implements Filter {

    @Resource
    private DynamicSecurityMetadataSource securityMetadataSource;

    @Resource
    public void setSecurityMetadataSource(DynamicAccessDecisionManager dynamicAccessDecisionManager) {
        super.setAccessDecisionManager(dynamicAccessDecisionManager);
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        FilterInvocation filterInvocation = new FilterInvocation(servletRequest, servletResponse, filterChain);
        InterceptorStatusToken interceptorStatusToken = super.beforeInvocation(filterInvocation);
        try {
            filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
        } finally {
            super.afterInvocation(interceptorStatusToken, null);
        }
    }

    @Override
    public SecurityMetadataSource obtainSecurityMetadataSource() {
        return this.securityMetadataSource;
    }

    @Override
    public Class getSecureObjectClass() {
        return FilterInvocation.class;
    }
}

最后加一个配置类,将所有的东西整合起来

(SpringSecurity 5.4.x以上新用法配置 为避免循环依赖,仅用于配置HttpSecurity)


import cn.hutool.json.JSONUtil;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.*;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.SecurityFilterChain;

import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsConfigurationSource;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;

import javax.annotation.Resource;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;


@Configuration
public class SpringSecurityConfig {

    @Resource
    private DynamicSecurityFilter dynamicSecurityFilter;

    @Resource
    private UserDetailsService userDetailsService;

    @Resource
    private DynamicAccessDecisionManager accessDecisionManager;

    @Resource
    private DynamicSecurityMetadataSource securityMetadataSource;

    /**
     * 配置过滤
     * @param httpSecurity
     * @return
     * @throws Exception
     */
    @Bean
    SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {

        /**
         *
         * authenticationEntryPoint:用来解决匿名用户访问无权限资源时的异常
         * accessDeniedHandler:用来解决认证过的用户访问无权限资源时的异常
         * authenticationSuccessHandler: 登陆成功
         * authenticationFailureHandler: 登陆失败
         */
        httpSecurity
                .cors().and().csrf().disable()
                .authorizeRequests()
                //这里配置是配置决策管理器和安全元数据,与过滤器相同,例如下面加了过滤器,并在过滤器中配置了决策管理器及安全数据源,这一段可以不加
                .withObjectPostProcessor(new ObjectPostProcessor() {
                     @Override
                     public  O postProcess(O o) {
                         //决策管理器
                         o.setAccessDecisionManager(accessDecisionManager);
                         //安全元数据源
                         o.setSecurityMetadataSource(securityMetadataSource);
                         return o;
                     }
                })
                //登出成功
                .and().logout().permitAll().logoutSuccessHandler((request, response, authenticationException) -> {
                    response.setContentType("application/json;charset=utf-8");
                    response.getWriter().write(JSONUtil.toJsonStr(Result.createBySuccess()));})
                    //登出成功,删除cookie
                    .deleteCookies("JSESSIONID")
                //设置登陆
                .and().formLogin().permitAll()
                    //登陆成功
                    .successHandler((request, response, accessDeniedException) -> {
                        //更新用户表上次登录时间、更新人、更新时间等字段
    //                    User userDetails = (User)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    //                    SysUser sysUser = sysUserService.selectByName(userDetails.getUsername());
    //                    sysUser.setLastLoginTime(new Date());
    //                    sysUser.setUpdateTime(new Date());
    //                    sysUser.setUpdateUser(sysUser.getId());
    //                    sysUserService.update(sysUser);

                        //此处还可以进行一些处理,比如登录成功之后可能需要返回给前台当前用户有哪些菜单权限,
                        //进而前台动态的控制菜单的显示等,具体根据自己的业务需求进行扩展
                        response.setContentType("text/json;charset=utf-8");
                        response.getWriter().write(JSONUtil.toJsonStr(Result.createBySuccess()));
                    })
                    //登陆失败
                    .failureHandler((request, response, authenticationException) -> {
                        response.setContentType("application/json;charset=utf-8");
                        //返回json数据
                        Result result = null;
                        if (authenticationException instanceof AccountExpiredException) {
                            //账号过期
    //                        result = ResultTool.fail(ResultCode.USER_ACCOUNT_EXPIRED);
                        } else if (authenticationException instanceof BadCredentialsException) {
                            //密码错误
    //                        result = ResultTool.fail(ResultCode.USER_CREDENTIALS_ERROR);
                        } else if (authenticationException instanceof CredentialsExpiredException) {
                            //密码过期
    //                        result = ResultTool.fail(ResultCode.USER_CREDENTIALS_EXPIRED);
                        } else if (authenticationException instanceof DisabledException) {
                            //账号不可用
    //                        result = ResultTool.fail(ResultCode.USER_ACCOUNT_DISABLE);
                        } else if (authenticationException instanceof LockedException) {
                            //账号锁定
    //                        result = ResultTool.fail(ResultCode.USER_ACCOUNT_LOCKED);
                        } else if (authenticationException instanceof InternalAuthenticationServiceException) {
                            //用户不存在
    //                        result = ResultTool.fail(ResultCode.USER_ACCOUNT_NOT_EXIST);
                        }else{
                            //其他错误
                            result = Result.createByError();
                        }
                        //处理编码方式,防止中文乱码的情况
                        response.setContentType("application/json;charset=utf-8");
                        //塞到HttpServletResponse中返回给前台
                        response.getWriter().write(JSONUtil.toJsonStr(result));
                    })
                //异常处理(权限拒绝、登录失效等)
                .and().exceptionHandling()
                    .authenticationEntryPoint((request, response, accessDeniedException) -> {
                        //处理匿名用户访问无权限资源时的异常(即未登录,或者登录状态过期失效)
                        response.setContentType("text/json;charset=utf-8");
                        response.getWriter().write(JSONUtil.toJsonStr(Result.createByInvalidParam()));
                    })
                    .accessDeniedHandler((request, response, accessDeniedException) -> {
                        //返回json形式的错误信息
                        response.setContentType("application/json;charset=utf-8");
                        response.getWriter().println("{\"code\":403,\"message\":\"小弟弟,你没有权限访问呀!\",\"data\":\"\"}");
                        response.getWriter().flush();
                    })
                //session会话管理,限制登录用户数量
                .and().sessionManagement().maximumSessions(1).expiredSessionStrategy((sessionInformationExpiredEvent) -> {
                    //会话信息过期策略
                    HttpServletResponse response = sessionInformationExpiredEvent.getResponse();
                    response.setContentType("application/json;charset=utf-8");
                    response.getWriter().write(JSONUtil.toJsonStr(Result.createByErrorMessage("会话信息过期")));
                });
        return httpSecurity.addFilterBefore(dynamicSecurityFilter, FilterSecurityInterceptor.class)
                .userDetailsService(userDetailsService)
                .build();

    }


    /**
     * 获取AuthenticationManager(认证管理器),登录时认证使用
     * @param authenticationConfiguration
     * @return
     * @throws Exception
     */
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    /**
     * 添加加密配置
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new PasswordEncoder() {
            @Override
            public String encode(CharSequence rawPassword) {
                return rawPassword.toString();
            }

            @Override
            public boolean matches(CharSequence rawPassword, String encodedPassword) {
                if (rawPassword == null) {
                    throw new IllegalArgumentException("rawPassword cannot be null");
                }
                if (encodedPassword == null || encodedPassword.length() == 0) {
                    return false;
                }
                if (!rawPassword.equals(encodedPassword)) {
                    return false;
                }
                return true;
            }
        };
    }
}

这样就完成了。具体一些配置,自己取探索。

你可能感兴趣的:(springboot,java,开发语言)