欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

springcurity前后端分离,使用动态请求url做鉴权及session共享

程序员文章站 2022-06-30 19:59:11
...

前言:

首先springcurity集成是不能直接适配前后端分离的, 需要简单的修改。
1.前后端分离登录接口需要返回登录成功或失败标识
2.无权限时,也需要返回无权限标识,而不是请求重定向
3.跨域处理

如何集成

增加maven依赖

 <dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
 </dependency>

配置类

package com.ljm.security.conf;

import com.ljm.security.api.SecurityApi;
import com.ljm.security.core.CustomAuthenticationEntryPoint;
import com.ljm.security.core.CustomAuthenticationProvider;
import com.ljm.security.core.CustomRoleVoter;
import com.ljm.security.filter.HandleCurUserFilter;
import com.ljm.security.filter.PreLoginFilter;
import com.ljm.security.handler.CustomAccessDeniedHandler;
import com.ljm.security.handler.CustomAuthenticationFailureHandler;
import com.ljm.security.handler.CustomAuthenticationSuccessHandler;
import com.ljm.security.handler.CustomLogoutSuccessHandler;
import com.ljm.security.loginprocessor.FormLoginPostProcessor;
import com.ljm.security.loginprocessor.JsonLoginPostProcessor;
import com.ljm.security.loginprocessor.LoginPostProcessor;
import org.springframework.boot.autoconfigure.session.SessionAutoConfiguration;
import org.springframework.boot.autoconfigure.session.SessionProperties;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.boot.web.servlet.server.Session;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Primary;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.vote.AuthenticatedVoter;
import org.springframework.security.access.vote.UnanimousBased;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.access.expression.WebExpressionVoter;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.session.security.web.authentication.SpringSessionRememberMeServices;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;
import org.springframework.util.ClassUtils;

import javax.annotation.Resource;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * @author: ChenHuaMing
 * @Date: 2020/6/10 16:55
 * @Description:
 */
@EnableConfigurationProperties({ ServerProperties.class})
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class CustomWebSecurityConfig extends WebSecurityConfigurerAdapter {
    private static final String REMEMBER_ME_SERVICES_CLASS = "org.springframework.security.web.authentication.RememberMeServices";
    @Resource
    private CustomAuthenticationProvider authenticationProvider;
    @Resource
    private CustomAccessDeniedHandler accessDeniedHandler;
    @Resource
    private CustomAuthenticationEntryPoint authenticationEntryPoint;
    @Resource
    private CustomAuthenticationSuccessHandler authenticationSuccessHandler;
    @Resource
    private CustomAuthenticationFailureHandler authenticationFailureHandler;
    @Resource
    private CustomLogoutSuccessHandler logoutSuccessHandler;
    @Resource
    @Lazy
    private SecurityApi securityApi;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) {
        auth.authenticationProvider(authenticationProvider);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/doLogin").permitAll()
                .antMatchers("/favicon.ico").permitAll()
                .accessDecisionManager(accessDecisionManager())
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginProcessingUrl("/doLogin")
                .failureHandler(authenticationFailureHandler)
                .successHandler(authenticationSuccessHandler)
                .and()
                .logout().logoutUrl("/logout")
                .logoutSuccessHandler(logoutSuccessHandler)
                .and()
                .exceptionHandling().accessDeniedHandler(accessDeniedHandler)
                .authenticationEntryPoint(authenticationEntryPoint)
                .and()
                .cors()
                .and()
                .addFilterBefore(preLoginFilter(), UsernamePasswordAuthenticationFilter.class)
                .addFilterBefore(new HandleCurUserFilter(), UsernamePasswordAuthenticationFilter.class)
                .csrf().disable();

    }

    @Bean
    @Primary
    public PasswordEncoder passwordEncoder(){
        return  new BCryptPasswordEncoder();
    }

    public PreLoginFilter preLoginFilter(){
        Set<LoginPostProcessor> set=new HashSet<>();
        set.add(new FormLoginPostProcessor());
        set.add(new JsonLoginPostProcessor());
        PreLoginFilter preLoginFilter=new PreLoginFilter("/doLogin",set);
        return preLoginFilter;
    }

    @Bean
    public AccessDecisionManager accessDecisionManager() {
        List<AccessDecisionVoter<? extends Object>> decisionVoters
                = Arrays.asList(
                new WebExpressionVoter(),
                // new RoleVoter(),
                new CustomRoleVoter(securityApi),
                new AuthenticatedVoter());
        return new UnanimousBased(decisionVoters);
    }

    @Bean
    public CookieSerializer httpSessionIdResolver(ServerProperties serverProperties){
        Session.Cookie cookie = serverProperties.getServlet().getSession().getCookie();
        DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
        PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
        map.from(cookie::getName).to(cookieSerializer::setCookieName);
        map.from(cookie::getDomain).to(cookieSerializer::setDomainName);
        map.from(cookie::getPath).to(cookieSerializer::setCookiePath);
        map.from(cookie::getHttpOnly).to(cookieSerializer::setUseHttpOnlyCookie);
        map.from(cookie::getSecure).to(cookieSerializer::setUseSecureCookie);
        map.from(cookie::getMaxAge).to((maxAge) -> cookieSerializer.setCookieMaxAge((int) maxAge.getSeconds()));
        if (ClassUtils.isPresent(REMEMBER_ME_SERVICES_CLASS, getClass().getClassLoader())) {
            new RememberMeServicesCookieSerializerCustomizer().apply(cookieSerializer);
        }
        cookieSerializer.setSameSite(null);
        return cookieSerializer;
    }

    /**
     * Customization for {@link SpringSessionRememberMeServices} that is only instantiated
     * when Spring Security is on the classpath.
     */
    static class RememberMeServicesCookieSerializerCustomizer {
        void apply(DefaultCookieSerializer cookieSerializer) {
            cookieSerializer.setRememberMeRequestAttribute(SpringSessionRememberMeServices.REMEMBER_ME_LOGIN_ATTR);
        }
    }
}

该配置类需要说明的一点是,由于某些浏览器高版本中增加了samesite等限制,不得不对cookie相关类做了下调整。

登录成功处理类

package com.ljm.security.handler;

import com.ljm.common.constants.SecurityConstants;
import com.ljm.security.util.SecurityResponseUtil;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author: ChenHuaMing
 * @Date: 2020/6/11 09:07
 * @Description: 登录成功处理类
 */
@Component
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException {
        httpServletRequest.getSession(true).setAttribute(SecurityConstants.CURRENT_LOGIN_NAME,authentication.getPrincipal().toString());
        httpServletResponse.setContentType("application/json;charset=UTF-8");
        httpServletResponse.getWriter().write(SecurityResponseUtil.handleResponseString(HttpServletResponse.SC_OK,"login success"));
    }
}

登录失败处理类

package com.ljm.security.handler;

import com.ljm.security.util.SecurityResponseUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author: ChenHuaMing
 * @Date: 2020/6/11 09:39
 * @Description: 处理登录失败类
 */
@Component
@Slf4j
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException {
        log.error("login fail",e);
        httpServletResponse.setContentType("application/json;charset=UTF-8");
        httpServletResponse.getWriter().write(SecurityResponseUtil.handleResponseString(HttpServletResponse.SC_FORBIDDEN,e.getMessage()));
    }
}

无权限访问处理类

package com.ljm.security.handler;

import com.ljm.security.util.SecurityResponseUtil;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author: ChenHuaMing
 * @Date: 2020/6/10 17:50
 * @Description: 处理无权限类
 */
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException {
        httpServletResponse.setContentType("application/json;charset=UTF-8");
        httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        httpServletResponse.getWriter().write(SecurityResponseUtil.handleResponseString(HttpServletResponse.SC_UNAUTHORIZED,"no auth please check"));
    }
}

登出处理类

package com.ljm.security.handler;

import com.ljm.security.util.SecurityResponseUtil;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;

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

/**
 * @author: ChenHuaMing
 * @Date: 2020/6/11 09:43
 * @Description: 登出处理类
 */
@Component
public class CustomLogoutSuccessHandler implements LogoutSuccessHandler {
    @Override
    public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        httpServletResponse.setContentType("application/json;charset=UTF-8");
        httpServletResponse.getWriter().write(SecurityResponseUtil.handleResponseString(HttpServletResponse.SC_FORBIDDEN,"no auth please check"));
    }
}

登录核心处理类

package com.ljm.security.core;

import org.springframework.context.annotation.Primary;
import org.springframework.security.authentication.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;

/**
 * @author: ChenHuaMing
 * @Date: 2020/6/10 17:45
 * @Description: 登录逻辑验证器
 */
@Component
@Primary
public class CustomAuthenticationProvider implements AuthenticationProvider {
    @Resource
    private UserDetailsService userDetailsService;
    @Resource
    private PasswordEncoder passwordEncoder;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        // 获取表单输入中返回的用户名
        String userName = authentication.getPrincipal().toString();
        // 获取表单中输入的密码
        String password = authentication.getCredentials().toString();
        //获取用户
        UserDetails userDetail = userDetailsService.loadUserByUsername(userName);
        //判断账号是否禁用
        if(userDetail.isEnabled()){
            throw new DisabledException("账号已禁用,请联系管理员");
        }
        //判断账号是否过期
        if(!userDetail.isAccountNonExpired()){
            throw new AccountExpiredException("账号已过期,请续期");
        }
        //判断账号是否锁定
        if(!userDetail.isAccountNonLocked()){
            throw new LockedException("账号已被锁定,请稍后重试");
        }
        //判断密码是否过期
        if(!userDetail.isCredentialsNonExpired()){
            throw new CredentialsExpiredException("密码已过期,请修改密码");
        }
        //检查密码
        if(!passwordEncoder.matches(password,userDetail.getPassword())){
            throw new BadCredentialsException("密码不正确,请重新输入");
        }

        // 构建返回的用户登录成功的token
        return new UsernamePasswordAuthenticationToken(userName, userDetail.getPassword(), userDetail.getAuthorities());
    }

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

动态url控制权限

主要是使用rolevoter实现,springsecurity原理是使用角色或者权限控制的,若需要实现动态控制url权限,可采用如下调整

package com.ljm.security.core;

import com.ljm.security.api.SecurityApi;
import com.ljm.security.dto.SecurityResRule;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.FilterInvocation;

import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

/**
 * @author: ChenHuaMing
 * @Date: 2020/6/23 11:23
 * @Description:
 */
public class CustomRoleVoter implements AccessDecisionVoter<Object> {
    private SecurityApi securityApi;

    public CustomRoleVoter(SecurityApi securityApi) {
        this.securityApi = securityApi;
    }

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

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

    @Override
    public int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) {
        if(authentication == null) {
            return ACCESS_DENIED;
        }
        int result = ACCESS_ABSTAIN;
        // 获取当前请求url
        String requestUrl = ((FilterInvocation) object).getHttpRequest().getServletPath();
        //获取权限规则
        SecurityResRule resRule = securityApi.getResRule(requestUrl);
        if(resRule!=null&&!resRule.isFdLoginVisible()){
            Collection<? extends GrantedAuthority> authorities = extractAuthorities(authentication);
            result=authorities.stream().anyMatch(auth->auth.getAuthority().equals(requestUrl))?ACCESS_GRANTED:ACCESS_DENIED;
        }
        return result;
    }

    private Collection<? extends GrantedAuthority> extractAuthorities(
            Authentication authentication) {
        return authentication.getAuthorities();
    }
}

session共享

session共享我使用了spring-session-data-redis
首先增加如下maven依赖

  <dependency>
     <groupId>org.springframework.session</groupId>
     <artifactId>spring-session-data-redis</artifactId>
  </dependency>
 <dependency>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-pool2</artifactId>
  <version>${common-pool2.version}</version>
 </dependency>

在yml中增加对应session共享配置
springcurity前后端分离,使用动态请求url做鉴权及session共享
如果采用redis共享方式还需要假如redis集成配置
springcurity前后端分离,使用动态请求url做鉴权及session共享
项目集成案例可参考
springcurity前后端分离,使用动态请求url做鉴权及session共享
源码地址:https://gitee.com/MingAndTao/ljm-simple-base

相关标签: 权限控制