1.添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2.创建User以及UserService
创建实体类UserDetailDTO类,该类需要实现UserDetails接口
@Data
@Builder
publicclassUserDetailDTOimplementsUserDetails {
/**
* 用户账号id
*/
privateIntegerid;
/**
* 用户信息id
*/
privateIntegeruserInfoId;
/**
* 邮箱号
*/
privateStringemail;
/**
* 登录方式
*/
privateIntegerloginType;
/**
* 用户名
*/
privateStringusername;
/**
* 密码
*/
privateStringpassword;
/**
* 用户角色
*/
privateList<String>roleList;
/**
* 用户昵称
*/
privateStringnickname;
/**
* 用户头像
*/
privateStringavatar;
/**
* 用户简介
*/
privateStringintro;
/**
* 个人网站
*/
privateStringwebSite;
/**
* 点赞文章集合
*/
privateSet<Object>articleLikeSet;
/**
* 点赞评论集合
*/
privateSet<Object>commentLikeSet;
/**
* 点赞说说集合
*/
privateSet<Object>talkLikeSet;
/**
* 用户登录ip
*/
privateStringipAddress;
/**
* ip来源
*/
privateStringipSource;
/**
* 是否禁用
*/
privateIntegerisDisable;
/**
* 浏览器
*/
privateStringbrowser;
/**
* 操作系统
*/
privateStringos;
/**
* 最近登录时间
*/
privateLocalDateTimelastLoginTime;
@Override
publicCollection<?extendsGrantedAuthority>getAuthorities() {
returnthis.roleList.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toSet());
}
@Override
publicStringgetPassword() {
returnthis.password;
}
@Override
publicStringgetUsername() {
returnthis.username;
}
@Override
publicbooleanisAccountNonExpired() {
returntrue;
}
@Override
publicbooleanisAccountNonLocked() {
returnthis.isDisable==FALSE;
}
@Override
publicbooleanisCredentialsNonExpired() {
returntrue;
}
@Override
publicbooleanisEnabled() {
returntrue;
}
}
getAuthorities方法的实现如上,直接从roleList中获取当前用户所具有的角色,构造SimpleGrantedAuthority然后返回即可
创建UserDetailsServiceImpl,用来执行登录等操作,UserDetailsServiceImpl需要实现UserDetailsService接口
@Service
publicclassUserDetailsServiceImplimplementsUserDetailsService {
@Autowired
privateUserAuthMapperuserAuthMapper;
@Autowired
privateUserInfoMapperuserInfoMapper;
@Autowired
privateRoleMapperroleMapper;
@Autowired
privateRedisServiceredisService;
@Resource
privateHttpServletRequestrequest;
@Override
publicUserDetailsloadUserByUsername(Stringusername) throwsUsernameNotFoundException {
if(StringUtils.isBlank(username)){
thrownewBizException("用户名不能为空");
}
//查询账号是否存在
UserAuthuserAuth=userAuthMapper.selectOne(newLambdaQueryWrapper<UserAuth>()
.select(UserAuth::getId, UserAuth::getUserInfoId, UserAuth::getUsername, UserAuth::getPassword, UserAuth::getLoginType)
.eq(UserAuth::getUsername, username));
if(Objects.isNull(userAuth)){
thrownewBizException("账号不存在");
}
// 封装登录信息
returnconvertUserDetail(userAuth, request);
}
/**
* 封装用户登录信息
*
* @param userAuth 用户账号
* @param request 请求
* @return 用户登录信息
*/
privateUserDetailsconvertUserDetail(UserAuthuserAuth, HttpServletRequestrequest) {
// 查询账号信息
UserInfouserInfo=userInfoMapper.selectById(userAuth.getUserInfoId());
// 查询账号角色
List<String>roleList=roleMapper.listRolesByUserInfoId(userInfo.getId());
System.out.println(roleList);
// 查询账号点赞信息
Set<Object>articleLikeSet=redisService.sMembers(ARTICLE_USER_LIKE+userInfo.getId());
Set<Object>commentLikeSet=redisService.sMembers(COMMENT_USER_LIKE+userInfo.getId());
Set<Object>talkLikeSet=redisService.sMembers(TALK_USER_LIKE+userInfo.getId());
// 获取设备信息
StringipAddress=IpUtils.getIpAddress(request);
StringipSource=IpUtils.getIpSource(ipAddress);
UserAgentuserAgent=IpUtils.getUserAgent(request);
// 封装权限集合
returnUserDetailDTO.builder()
.id(userAuth.getId())
.loginType(userAuth.getLoginType())
.userInfoId(userInfo.getId())
.username(userAuth.getUsername())
.password(userAuth.getPassword())
.email(userInfo.getEmail())
.roleList(roleList)
.nickname(userInfo.getNickname())
.avatar(userInfo.getAvatar())
.intro(userInfo.getIntro())
.webSite(userInfo.getWebSite())
.articleLikeSet(articleLikeSet)
.commentLikeSet(commentLikeSet)
.talkLikeSet(talkLikeSet)
.ipAddress(ipAddress)
.ipSource(ipSource)
.isDisable(userInfo.getIsDisable())
.browser(userAgent.getBrowser().getName())
.os(userAgent.getOperatingSystem().getName())
.lastLoginTime(LocalDateTime.now(ZoneId.of(SHANGHAI.getZone())))
.build();
}
}
这里实现了UserDetailsService接口中的loadUserByUsername方法,在执行登录的过程中,这个方法将根据用户名去查找用户,如果用户名不存在,则抛出异常,用户名存在的话,需要封装用户登陆信息并返回。
3.自定义FilterInvocationSecurityMetadataSource
FilterInvocationSecurityMetadataSource有一个默认的实现类DefaultFilterInvocationSecurityMetadataSource,该类的主要功能就是通过当前的请求地址,获取该地址需要的用户角色。可以自己也定义一个FilterInvocationSecurityMetadataSource实现同样的功能
/**
* 接口拦截规则
*/
@Component
publicclassFilterInvocationSecurityMetadataSourceImplimplementsFilterInvocationSecurityMetadataSource {
/**
* 资源角色列表
*/
privatestaticList<ResourceRoleDTO>resourceRoleList;
@Autowired
privateRoleMapperroleMapper;
/**
* 加载资源角色信息
*/
@PostConstruct
privatevoidloadDataSource() {
resourceRoleList=roleMapper.listResourceRoles();
}
/**
* 清空接口角色信息
*/
publicvoidclearDataSource() {
resourceRoleList=null;
}
@Override
publicCollection<ConfigAttribute>getAttributes(Objectobject) throwsIllegalArgumentException {
// 修改接口角色关系后重新加载
if (CollectionUtils.isEmpty(resourceRoleList)) {
this.loadDataSource();
}
FilterInvocationfi= (FilterInvocation) object;
// 获取用户请求方式
Stringmethod=fi.getRequest().getMethod();
// 获取用户请求Url
Stringurl=fi.getRequest().getRequestURI();
AntPathMatcherantPathMatcher=newAntPathMatcher();
// 获取接口角色信息,若为匿名接口则放行,若无对应角色则禁止
for (ResourceRoleDTOresourceRoleDTO : resourceRoleList) {
if (antPathMatcher.match(resourceRoleDTO.getUrl(), url) &&resourceRoleDTO.getRequestMethod().equals(method)) {
List<String>roleList=resourceRoleDTO.getRoleList();
if (CollectionUtils.isEmpty(roleList)) {
returnSecurityConfig.createList("disable");
}
returnSecurityConfig.createList(roleList.toArray(newString[]{}));
}
}
returnnull;
}
@Override
publicCollection<ConfigAttribute>getAllConfigAttributes() {
returnnull;
}
@Override
publicbooleansupports(Class<?>aClass) {
returnFilterInvocation.class.isAssignableFrom(aClass);
}
}
如果getAttributes(Object o)方法返回null的话,意味着当前这个请求不需要任何角色就能访问,甚至不需要登录。我这个项目中有匿名接口,匿名接口不需要任何角色,可以直接访问。
不是匿名接口的话,将访问接口需要的角色信息封装成Collection<ConfigAttribute>并返回,getAttributes(Object o)方法返回的集合最终会来到AccessDecisionManager类中,接下来我们再来看AccessDecisionManager类
4.自定义AccessDecisionManager
自定义AccessDecisionManagerImpl类实现AccessDecisionManager接口
/**
* 访问决策管理器
*/
@Component
publicclassAccessDecisionManagerImplimplementsAccessDecisionManager {
@Override
publicvoiddecide(Authenticationauthentication, Objecto, Collection<ConfigAttribute>collection) throwsAccessDeniedException, InsufficientAuthenticationException {
// 获取当前登录用户权限列表
List<String>permissionList=authentication.getAuthorities()
.stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList());
for (ConfigAttributeitem : collection) {
if (permissionList.contains(item.getAttribute())) {
return;
}
}
thrownewAccessDeniedException("没有操作权限");
}
@Override
publicbooleansupports(ConfigAttributeconfigAttribute) {
returntrue;
}
@Override
publicbooleansupports(Class<?>aClass) {
returntrue;
}
}
这里涉及到一个all和any的问题:假设当前登录用户具备角色A、角色B,当前请求需要角色B、角色C,那么是要当前用户要包含所有请求角色才算授权成功,还是只要包含一个就算授权成功?这里采用了第二种方案,即只要包含一个即可。可根据自己的实际情况调整decide方法中的逻辑。
5.自定义AccessDeniedHandler
/**
* 用户权限处理
*
*/
@Component
publicclassAccessDeniedHandlerImplimplementsAccessDeniedHandler {
@Override
publicvoidhandle(HttpServletRequesthttpServletRequest, HttpServletResponsehttpServletResponse, AccessDeniedExceptione) throwsIOException {
httpServletResponse.setContentType(APPLICATION_JSON);
httpServletResponse.getWriter().write(JSON.toJSONString(Result.fail("权限不足")));
}
}
6.配置WebSecurityConfig
/**
* securiy配置类
*
*/
@Configuration
@EnableWebSecurity
publicclassWebSecurityConfigextendsWebSecurityConfigurerAdapter {
@Autowired
privateAuthenticationSuccessHandlerImplauthenticationSuccessHandler;
@Autowired
privateAuthenticationFailHandlerImplauthenticationFailHandler;
@Autowired
privateLogoutSuccessHandlerImpllogoutSuccessHandler;
@Autowired
privateAuthenticationEntryPointImplauthenticationEntryPoint;
@Autowired
privateAccessDeniedHandleraccessDeniedHandler;
@Bean
publicFilterInvocationSecurityMetadataSourcesecurityMetadataSource(){
returnnewFilterInvocationSecurityMetadataSourceImpl();
}
@Bean
publicAccessDecisionManageraccessDecisionManager(){
returnnewAccessDecisionManagerImpl();
}
@Bean
publicSessionRegistrysessionRegistry() {
returnnewSessionRegistryImpl();
}
//防止用户重复登录
@Bean
publicHttpSessionEventPublisherhttpSessionEventPublisher() {
returnnewHttpSessionEventPublisher();
}
/**
* 密码加密
*
* @return {@link PasswordEncoder} 加密方式
*/
@Bean
publicPasswordEncoderpasswordEncoder() {
returnnewBCryptPasswordEncoder();
}
/**
* 配置权限
* @param http
* @throws Exception
*/
@Override
protectedvoidconfigure(HttpSecurityhttp) throwsException {
//配置登录注销的路径
http.formLogin()
.loginProcessingUrl("/login")
.successHandler(authenticationSuccessHandler)
.failureHandler(authenticationFailHandler)
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessHandler(logoutSuccessHandler);
// 配置路由权限信息
http.authorizeRequests()
/*
配置路由权限信息
通过withObjectPostProcessor将刚刚创建的FilterInvocationSecurityMetadataSourceImpl
和AccessDecisionManagerImpl注入进来。到时候,请求都会经过刚才的过滤器(
除了configure(WebSecurity web)方法忽略的请求)
*/
.withObjectPostProcessor(newObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public<OextendsFilterSecurityInterceptor>OpostProcess(Ofsi) {
fsi.setSecurityMetadataSource(securityMetadataSource());
fsi.setAccessDecisionManager(accessDecisionManager());
returnfsi;
}
})
//这里permitAll一定要加,否则启动会报错
.anyRequest().permitAll()
.and()
.csrf().disable().exceptionHandling()
//未登录处理
.authenticationEntryPoint(authenticationEntryPoint)
//权限不足处理
.accessDeniedHandler(accessDeniedHandler)
.and()
.sessionManagement()
.maximumSessions(20)
.sessionRegistry(sessionRegistry());
}
}