版本: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 extends GrantedAuthority> 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
接下来我们需要一个存有所有权限的数据源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;
}
};
}
}
这样就完成了。具体一些配置,自己取探索。