目录
1.简单介绍权限数据库设置
1.1重点讲解
2. 自定义UserDetails
2.1.重点讲解
3. 自定义UserDetailsService
3.1 重点讲解
4.自定义 FilterInvocationSecurityMetadataSource
4.1重点讲解
5.自定义 AccessDecisionManager
5.1 重点讲解
6.配置WebSecurityConfigurerAdapter
6.1.重点讲解
7.效果展示
package com.cr.mdsp.common.security.entity;
import com.bcy.ipg.user.model.Role;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
@Data
public class SecurityUser implements UserDetails {
private static final long serialVersionUID = 1L;
private Integer id;
/**
* 账号
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 企业微信的成员UserId
*/
private String qywxUserId;
/**
* 用户真实名称
*/
private String realName;
/**
* 企业微信的成员别名
*/
private String aliasName;
/**
* 头像
*/
private String headImgUrl;
/**
* 手机号码
*/
private String phone;
/**
* 邮箱
*/
private String email;
/**
* 地址
*/
private String address;
/**
* 启用/禁用成员。1表示启用成员,0表示禁用成员
*/
private Integer enabled;
/**
* 用户所对应的角色
*/
private List roles;
/**
*即直接从 roles 中获取当前用户所具有的角色,构造 SimpleGrantedAuthority 然后返回即可。
*/
@Override
@JsonInclude
public Collection extends GrantedAuthority> getAuthorities() {
List authorities = new ArrayList<>();
for (Role role : roles) {
authorities.add(new SimpleGrantedAuthority(role.getName()));
}
return authorities;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return enabled == 1 ? true : false;
}
}
UserDetails 接口默认有几个方法需要实现,这几个方法中,除了 isEnabled 返回了正常的 enabled之外,其他的方法我都统一返回 true,因为我这里的业务逻辑并不涉及到账户的锁定、密码的过期等等, 只有账户是否被禁用,因此只处理了isEnabled 方法,这一块小伙伴可以根据自己的实际情况来调整。
package com.cr.dgp.web.config.security;
import com.bcy.ipg.user.model.User;
import com.bcy.ipg.user.service.RoleService;
import com.bcy.ipg.user.service.UserService;
import com.cr.mdsp.common.security.entity.SecurityUser;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
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 {
@Autowired
private UserService userService;
@Autowired
private RoleService roleService;
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
//根据用户名查询数据
User user = userService.getUserByUsername(userName);
if (user == null) {
throw new UsernameNotFoundException("用户不存在");
}
SecurityUser securityUser = new SecurityUser();
BeanUtils.copyProperties(user, securityUser);
//在此处塞入该用户所对应的权限,可以在其他地方设入,其他方法自行查阅
securityUser.setRoles(roleService.getRoleListByUserId(null, user.getId()));
return securityUser;
}
}
这里最主要是实现了 UserDetailsService 接口中的 loadUserByUsername 方法,在执行登录的过程中,这个方法将根据用户名去查找用户,如果用户不存在,则抛UsernameNotFoundException 异常,否则直接将查到的用户返回。
UserService用来执行数据库的查询操作, 本文的重点不在数据库查询所以不做详解了,只简单描述方法的用处。
FilterInvocationSecurityMetadataSource 有一个默认的实现类DefaultFilterInvocationSecurityMetadataSource,该类的主要功能就是通过当前的请求地址,获取该地址需要的用户角色,咱们有样学样,自己也定义一下FilterInvocationSecurityMetadataSource,如下:
package com.cr.dgp.web.filter.security;
import com.bcy.ipg.user.model.Menu;
import com.bcy.ipg.user.model.Role;
import com.bcy.ipg.user.model.RoleMenu;
import com.bcy.ipg.user.service.MenuService;
import com.bcy.ipg.user.service.RoleMenuService;
import com.bcy.ipg.user.service.RoleService;
import org.apache.metamodel.util.CollectionUtils;
import org.pentaho.di.core.util.StringUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
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 java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@Component
public class CustomFilterInvocationSecurityMetadataSource implements
FilterInvocationSecurityMetadataSource {
@Autowired
MenuService menuService;
AntPathMatcher antPathMatcher = new AntPathMatcher();
@Autowired
private RoleMenuService roleMenuService;
@Autowired
private RoleService roleService;
@Override
public Collection getAttributes(Object object) throws IllegalArgumentException {
String requestUrl = ((FilterInvocation) object).getRequestUrl();
if (antPathMatcher.match("/login*", requestUrl)) {
return null;
}
List
(1) 一开始注入了 MenuService,MenuService 的作用是用来查询数据库中 url pattern 和 role 的对应关系,查询结果是一个 List 集合,集合中是 Menu 类,Menu 类有两个核心属性,一个是 urlpattern,即匹配规则(比如 /admin/** ),还有一个是 List,即这种规则的路径需要哪些角色才能
访问。
(2)我们可以从 getAttributes(Object o) 方法的参数 o 中提取出当前的请求 url,然后将这个请求 url和数据库中查询出来的所有 url pattern一一对照,看符合哪一个url pattern,然后就获取到该
url pattern 所对应的角色,当然这个角色可能有多个,所以遍历角色,最后利用SecurityConfig.createList 方法来创建一个角色集合。
(3)第二步的操作中,涉及到一个优先级问题,比如我的地址是 /employee/basic/hello ,这个地址
既能被 /employee/** 匹配,也能被 /employee/basic/** 匹配,这就要求我们从数据库查询
的时候对数据进行排序,将 /employee/basic/** 类型的 url pattern 放在集合的前面去比较。
(4) 如果 getAttributes(Object o) 方法返回 null 的话,意味着当前这个请求不需要任何角色就能访问,甚至不需要登录。但是在我的整个业务中,并不存在这样的请求,我这里的要求是,所有未匹配到的路径,都是认证(登录)后可访问,因此我在这里返回一个 ROLE_LOGIN 的角色,这种角色在我的角色数据库中并不存在,因此我将在下一步的角色比对过程中特殊处理这种角色。
(5) 如果地址是 /login,这个是登录页,不需要任何角色即可访问,直接返回 null。
(6) getAttributes(Object o) 方法返回的集合最终会来到 AccessDecisionManager 类中,接下来我们再来看 AccessDecisionManager 类。
package com.cr.dgp.web.filter.security;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;
import java.util.Collection;
@Component
public class CustomAccessDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object object, Collection configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
for (ConfigAttribute configAttribute : configAttributes) {
//当前请求需要的权限
String needRole = configAttribute.getAttribute();
if ("ROLE_LOGIN".equals(needRole)) {
if (authentication instanceof AnonymousAuthenticationToken) {
throw new AccessDeniedException("尚未登录,请登录!");
} else {
return;
}
}
//当前用户所具有的权限
Collection extends GrantedAuthority> authorities = authentication.getAuthorities();
for (GrantedAuthority authority : authorities) {
if (authority.getAuthority().equals(needRole)) {
return;
}
}
}
throw new AccessDeniedException("权限不足,请联系管理员!");
}
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
@Override
public boolean supports(Class> clazz) {
return true;
}
}
(1) decide 方法接收三个参数,其中第一个参数中保存了当前登录用户的角色信息,第三个参数则是UrlFilterInvocationSecurityMetadataSource 中的 getAttributes 方法传来的,表示当前请求需要的角色(可能有多个)。
(2) 如果当前请求需要的权限为 ROLE_LOGIN 则表示登录即可访问,和角色没有关系,此时我需要判断 authentication 是不是 AnonymousAuthenticationToken 的一个实例,如果是,则表示当前用户没有登录,没有登录就抛一个 BadCredentialsException 异常,登录了就直接返回,则这个
请求将被成功执行。
(3)遍历 collection,同时查看当前用户的角色列表中是否具备需要的权限,如果具备就直接返回,否则就抛异常。
(4) 这里涉及到一个 all 和 any 的问题:假设当前用户具备角色 A、角色 B,当前请求需要角色 B、角色 C,那么是要当前用户要包含所有请求角色才算授权成功还是只要包含一个就算授权成功?我这里采用了第二种方案,即只要包含一个即可。小伙伴可根据自己的实际情况调整 decide 方法中的逻辑。
最后在 webSecurityConfig 中完成简单的配置即可,如下:
package com.cr.dgp.web.config.security;
import com.cr.dgp.web.filter.security.CustomAccessDecisionManager;
import com.cr.dgp.web.filter.security.CustomFilterInvocationSecurityMetadataSource;
import com.xxl.job.core.biz.AdminBiz;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.web.context.request.RequestContextListener;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomFilterInvocationSecurityMetadataSource filterInvocationSecurityMetadataSource;
@Autowired
private CustomAccessDecisionManager accessDecisionManager;
private UserDetailsService userDetailsService;
@Autowired
public void setUserDetailsService(UserDetailsServiceImpl userDetailsService) {
this.userDetailsService = userDetailsService;
}
@Bean
public RequestContextListener requestContextListener() {
return new RequestContextListener();
}
@Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers("/resources/**");
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.exceptionHandling().authenticationEntryPoint(new LoginUrlEntryPoint("/login"))
.and()
.headers()
.frameOptions()
.sameOrigin()
.and()
.authorizeRequests().withObjectPostProcessor(new ObjectPostProcessor() {
@Override
public O postProcess(O o) {
o.setAccessDecisionManager(accessDecisionManager);
o.setSecurityMetadataSource(filterInvocationSecurityMetadataSource);
return o;
}
})
.antMatchers("/", "/" + AdminBiz.MAPPING, "/prometheus").permitAll()
.anyRequest()
.authenticated()
.and()
.formLogin()
.usernameParameter("username")
.passwordParameter("password")
.loginPage("/login")
.loginProcessingUrl("/login")
.successHandler(new LoginSuccessHandler())
.failureUrl("/login?error")
.permitAll()
.and()
.logout()
.logoutUrl("/login?logout")
.and()
.sessionManagement()
.sessionFixation()
.newSession()
.maximumSessions(-1)
.maxSessionsPreventsLogin(true)
.expiredUrl("/timeout")
.sessionRegistry(sessionRegistry());
}
@Bean
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
}
灵感来源:
特别鸣谢一位名为:“江南一点雨” 作者所编写的开源项目 微人事