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共享配置
如果采用redis共享方式还需要假如redis集成配置
项目集成案例可参考
源码地址:https://gitee.com/MingAndTao/ljm-simple-base