• spring boot+spring security 使用随笔


    本人最近接受的新任务是从零搭一个管理系统的权限管理模块,从零开始学习了spring security,已完成绝大部分设计和开发,和大家分享一些学习和开发心得.

    首先是数据库的设计,这里是

     按照产品的需求,本权限管理模块,权限同时与角色和用户关联,要求角色有普通用户和管理员,用户和权限之间做直接的关联,我采用的是在权限校验时,先查询是否是管理员,如果不是则查询"用户--权限表".

    接下来用Spring Boot配置Spring Security.

    自定义一个SecurityConfig实现WebSecurityConfigurerAdapter,做权限核心控制类

    @Configuration
    @EnableWebSecurity //启用web权限
    @EnableGlobalMethodSecurity(prePostEnabled = true) //启用方法验证
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    // @Autowired
    // private CasProperties casProperties;
    @Autowired
    CustomUserDetailsService customUserDetailsService;

    @Autowired
    MyFilterInvocationSecurityMetadataSource myFilterInvocationSecurityMetadataSource;
    @Autowired
    MyAccessDecisionManager myAccessDecisionManager;
    @Autowired
    AuthenticationAccessDeniedHandler authenticationAccessDeniedHandler;
    @Autowired
    CustomizeAuthenticationEntryPoint authenticationEntryPoint;
    @Autowired
    CustomizeAuthenticationFailureHandler authenticationFailureHandler;
    @Autowired
    CustomizeSessionInformationExpiredStrategy sessionInformationExpiredStrategy;




    /**定义认证用户信息获取来源,密码校验规则等*/
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    super.configure(auth);

    }

    /**
    * 定义安全策略
    * @param http
    * @throws Exception
    */
    @Override
    protected void configure(HttpSecurity http) throws Exception {

    http.authorizeRequests()//配置安全策略
    .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
    @Override
    public <O extends FilterSecurityInterceptor> O postProcess(O o) {
    o.setSecurityMetadataSource(myFilterInvocationSecurityMetadataSource);
    o.setAccessDecisionManager(myAccessDecisionManager);
    return o;
    }
    })
    .anyRequest().authenticated()//其余的所有请求都需要验证
    .and()
    .logout()
    .permitAll()//定义logout不需要验证
    .and()
    //把自定义的验证码过滤器添加到验证用户名密码的过滤器前
    // .addFilterBefore(new VerifyFilter(), UsernamePasswordAuthenticationFilter.class)
    .formLogin()
    // .loginProcessingUrl("/login")
    .loginProcessingUrl(UrlConstant.LOGIN_URL)
    .usernameParameter("userId").passwordParameter("password")
    .defaultSuccessUrl(UrlConstant.DEFAULT_SUCCESS_Url)
    .permitAll().//允许所有用户
    failureHandler(authenticationFailureHandler);
    //登录失败处理逻辑
    //异常处理(权限拒绝、登录失效等)
    http.exceptionHandling().
    authenticationEntryPoint(authenticationEntryPoint)//匿名用户访问无权限资源时的异常处理and()
    .and()
    .cors()//新加入
    .and()
    .csrf().disable();
    http.cors()
    .and()
    .exceptionHandling().accessDeniedHandler(authenticationAccessDeniedHandler)
    //会话管理
    .and().sessionManagement().
    maximumSessions(3).//同一账号同时登录最大用户数
    expiredSessionStrategy(sessionInformationExpiredStrategy);//会话信息过期策略会话信息过期策略(账号被挤下线)
    // http.csrf().disable();//关闭CSRF保护
    }

    /**
    * 默认的账号登录认证策略
    * @return
    */
    @Bean
    public AbstractUserDetailsAuthenticationProvider usernameAndPasswordAuthenticationProvider(){
    DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
    daoAuthenticationProvider.setUserDetailsService( accountDetailsService() );
    daoAuthenticationProvider.setPasswordEncoder( bCryptPasswordEncoder() );//设置密码策略
    return daoAuthenticationProvider;
    }

    /**
    * 注册获取用户权限Bean
    * @return
    */
    @Bean
    public CustomUserDetailsService accountDetailsService(){
    return new CustomUserDetailsService();
    }
    /**
    * 注册md5加密Bean
    * @return
    */
    @Bean
    public MessageDigestPasswordEncoder bCryptPasswordEncoder(){
    return new MessageDigestPasswordEncoder("MD5");
    }
    }

    在这个类中configure(HttpSecurity http)方法是最核心的方法,我在这个方法中配置了,登录访问的路径
    登录访问的路径
    .loginProcessingUrl(UrlConstant.LOGIN_URL)
    ,登录成功的调用的方法
    .defaultSuccessUrl(UrlConstant.DEFAULT_SUCCESS_Url)
    ,失败调用的方法
    failureHandler(authenticationFailureHandler);
    ,无权限时调用的方法
    .exceptionHandling().accessDeniedHandler(authenticationAccessDeniedHandler)
    匿名用户访问无权限资源时的异常处理
    .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint)
    //会话管理
    .and().sessionManagement().
    maximumSessions(3).//同一账号同时登录最大用户数
    expiredSessionStrategy(sessionInformationExpiredStrategy);//会话信息过期策略会话信息过期策略(账号被挤下线)
    接着配置其他的处理类:
    /**
    * 拒绝访问处理
    */
    @Component
    public class AuthenticationAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse resp, AccessDeniedException e) throws IOException, ServletException {
    resp.setStatus(HttpServletResponse.SC_FORBIDDEN);
    resp.setContentType("application/json;charset=UTF-8");
    PrintWriter out = resp.getWriter();
    out.write("{"status":"error","msg":"权限不足,请联系管理员!"}");
    out.flush();
    out.close();
    }
    }

    /**
    * @Description: 匿名用户访问无权限资源时的异常
    * @Date Create in 2019/9/3 21:35
    */
    @Component
    public class CustomizeAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
    JsonResult result = ResultTool.fail(ResultCode.USER_NOT_LOGIN);
    httpServletResponse.setContentType("text/json;charset=utf-8");
    httpServletResponse.getWriter().write(JSON.toJSONString(result));
    }
    }
    /**
    * @Author: cg
    * @Description: 登录失败处理逻辑
    */
    @Component
    public class CustomizeAuthenticationFailureHandler implements AuthenticationFailureHandler {


    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException{
    //返回json数据
    JsonResult result = null;
    if (e instanceof VerifyCodeException){
    result = ResultTool.fail(ResultCode.USER_VERIFICATION_CODE_ERROR);
    }else if (e instanceof AccountExpiredException) {
    //账号过期
    result = ResultTool.fail(ResultCode.USER_ACCOUNT_EXPIRED);
    } else if (e instanceof BadCredentialsException) {
    //密码错误
    result = ResultTool.fail(ResultCode.USER_CREDENTIALS_ERROR);
    } else if (e instanceof CredentialsExpiredException) {
    //密码过期
    result = ResultTool.fail(ResultCode.USER_CREDENTIALS_EXPIRED);
    } else if (e instanceof DisabledException) {
    //账号不可用
    result = ResultTool.fail(ResultCode.USER_ACCOUNT_DISABLE);
    } else if (e instanceof LockedException) {
    //账号锁定
    result = ResultTool.fail(ResultCode.USER_ACCOUNT_LOCKED);
    } else if (e instanceof InternalAuthenticationServiceException) {
    //用户不存在
    result = ResultTool.fail(ResultCode.USER_ACCOUNT_NOT_EXIST);
    }else{
    //其他错误
    result = ResultTool.fail(ResultCode.COMMON_FAIL);
    }
    //处理编码方式,防止中文乱码的情况
    httpServletResponse.setContentType("text/json;charset=utf-8");
    //塞到HttpServletResponse中返回给前台
    httpServletResponse.getWriter().write(JSON.toJSONString(result));
    }
    }
    /**
    * @Author: cg
    * @Description: 会话信息过期策略
    */
    @Component
    public class CustomizeSessionInformationExpiredStrategy implements SessionInformationExpiredStrategy {
    @Override
    public void onExpiredSessionDetected(SessionInformationExpiredEvent sessionInformationExpiredEvent) throws IOException, ServletException {
    JsonResult result = ResultTool.fail(ResultCode.USER_ACCOUNT_USE_BY_OTHERS);
    HttpServletResponse httpServletResponse = sessionInformationExpiredEvent.getResponse();
    httpServletResponse.setContentType("text/json;charset=utf-8");
    httpServletResponse.getWriter().write(JSON.toJSONString(result));
    }
    }

    MyAccessDecisionManager 是认证核心处理类,校验访问的接口所对应的权限,登录的用户是否拥有
    /**
    * 认证核心类
    */
    @Service
    public class MyAccessDecisionManager implements AccessDecisionManager {
    /**
    * decide 方法是判定是否拥有权限的决策方法,authentication是CustomUserService
    * 中循环添加到 GrantedAuthority 对象中的权限信息集合,object 包含客户端发起的请求的requset信息,
    * 可转换为 HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
    * configAttributes为MyFilterInvocationSecurityMetadataSource的getAttributes(Object object)
    * 这个方法返回的结果.
    *
    */
    @Override
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {

    if(null== configAttributes || configAttributes.size() <=0) {
    return;
    }
    ConfigAttribute c;
    String needRole;
    //遍历用户已有的权限中是否有这一个接口所对应的的权限
    for(Iterator<ConfigAttribute> iter = configAttributes.iterator(); iter.hasNext(); ) {
    c = iter.next();
    needRole = c.getAttribute();
    for(GrantedAuthority ga : authentication.getAuthorities()) {//authentication 为在注释1 中循环添加到 GrantedAuthority 对象中的权限信息集合
    //比较是否是该接口所需的权限
    if(needRole.trim().equals(ga.getAuthority())) {
    return;
    }
    }
    }
    throw new AccessDeniedException("no right");
    }

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

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


    我通过MyFilterInvocationSecurityMetadataSource加载url权限表,把每一个接口对应的权限存储到数据库,在服务器启动时调用,
    把url和对应的权限加载到内存中,在用户每一次访问被 接口时校验该接口是否被存到数据库中,如果被存储到了数据库就得到其对应的权限,
    如果数据库中没有这个url的地址,则放行不进行拦截
    /**
    * 加载权限表及判断url权限类
    */
    @Service
    public class MyFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

    @Autowired
    PermissionMapper permissionMapper;

    private HashMap<String, Collection<ConfigAttribute>> map = null;

    /**
    * 加载权限表中所有权限
    */
    public void loadResourceDefine() {
    map = new HashMap<String, Collection<ConfigAttribute>>();
    //获取url_auth表中所有的url和权限名
    List<AuthUrlPermission> authUrlPermissions = permissionMapper.selectUrlPerm();
    //把所有的url和对应的权限名添加到内存的map中,用于后面的权限对比
    for (AuthUrlPermission permission : authUrlPermissions) {
    ConfigAttribute cfg = new SecurityConfig(permission.getAuthName());
    List<ConfigAttribute> list = new ArrayList<>();
    list.add(cfg);
    //用于restful风格的接口地址和请求方法拼接起来存入内存的map中
    map.put(permission.getUrlName()+":"+permission.getMethod(), list);
    }


    }

    /**
    * 此方法是为了判定用户请求的url 是否在权限表中,如果在权限表中,则返回给 decide 方法, 用来判定用户
    * 是否有此权限。如果不在权限表中则放行。
    */
    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
    //如果内存中存储url和权限的map为null则去调用加载map方法
    if (map == null) {
    loadResourceDefine();
    }
    // object 中包含用户请求的request的信息
    HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
    for (Map.Entry<String, Collection<ConfigAttribute>> entry : map.entrySet()) {
    String urlAndMethod = entry.getKey();
    //把key中的url和请求方法通过:切割开
    String url = urlAndMethod.split(":")[0];
    String method = urlAndMethod.split(":")[1];
    //验证url和请求方法与请求中的是否一致
    if (new AntPathRequestMatcher(url).matches(request) &&
    request.getMethod().equals(method)) {
    //如果url和请求方法与请求中的一致.则返回该请求对应的权限名
    return map.get(urlAndMethod);
    }
    }
    //如果请求的地址和方法不在存储的url表中则不做拦截
    return null;
    }

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

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

    从数据库获取用户信息和用户所拥有的的权限,提供给AuthenticationManager进行登录校验
    /**
    * 从数据库获取用户权限类
    */
    @Service("customUserDetailsService")
    public class CustomUserDetailsService implements UserDetailsService {
    @Autowired
    PermissionMapper permissionMapper;
    @Autowired
    UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    System.out.println("当前的用户名是:"+username);
    //根据用户名从数据库用户所拥有的权限
    AuthUser authUser2 = userMapper.userLogin(username);
    if (authUser2 == null){
    throw new UsernameNotFoundException("用户名不存在");
    }
    Set<GrantedAuthority> authorities = new HashSet<>();
    List<PermissionCheckVO> permissionCheckVOS = permissionMapper.selectCheckPerm(username);
    //添加用户默认开启的权限
    authorities.add(new SimpleGrantedAuthority("USER"));
    authorities.add(new SimpleGrantedAuthority("processList"));
    authorities.add(new SimpleGrantedAuthority("processList_edit"));
    authorities.add(new SimpleGrantedAuthority("process_approval_child"));
    authorities.add(new SimpleGrantedAuthority("process_approval_child_edit"));
    //遍历添加权限
    for (PermissionCheckVO r: permissionCheckVOS) {
    //添加编辑权限
    if (r.getEditStatus()!=null && r.getEditStatus().equals(true)){
    authorities.add(new SimpleGrantedAuthority(r.getAuthName()));
    r.setAuthName(r.getAuthName()+"_edit");
    }
    authorities.add(new SimpleGrantedAuthority(r.getAuthName()));
    }
    //把数据库中的用户名和密码以及权限返回给AuthenticationManager调用
    User user = new User(username, authUser2.getPassword(), authorities);
    return user;
    }
    }
    以上是我的springboot+spring security的配置

     
  • 相关阅读:
    Wiggle Sort II
    Coin Change
    MPLS LDP 知识要点
    MPLS Aggreate & Untag
    Lab MPLS隐藏标签显示
    Lab MPLS过滤标签转发
    MPLS MTU Aggregation
    研究MPLS MTU的问题
    Lab 利用MPLS解决BGP路由黑洞
    MPLS 标签保留
  • 原文地址:https://www.cnblogs.com/cg14/p/12102884.html
Copyright © 2020-2023  润新知