Demo源码:https://github.com/ygsama/ipa
需要实现三个接口
UserDetails 用户类接口
UserDetailsService 查询用户密码的service 接口
AuthenticationProvider 为认证管理器AuthenticationManager 提供验证组件AuthenticationProvider
/**
* 实现了 {@link UserDetails}接口
* 用于构建存储在SecurityContextHolder的Authentication对象
*
* @author ygsama
*/
@Slf4j
@Data
public class SysUserDO implements UserDetails {
private String username;
private String password;
private String name;
private List<SysRoleDO> roleList;
// ... 其他字段省略
/**
* 装填用户的角色列表
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if (roleList == null || roleList.size() < 1) {
return AuthorityUtils.commaSeparatedStringToAuthorityList("");
}
log.info("[原始用户角色列表装填]: ", roleList);
StringBuilder roles = new StringBuilder();
for (SysRoleDO role : roleList) {
roles.append("ROLE_").append(role.getNo()).append(",");
}
List<GrantedAuthority> authorityList = AuthorityUtils.commaSeparatedStringToAuthorityList(roles.substring(0, roles.length() - 1));
log.info("[遍历并返回用户的角色列表]: {}", authorityList);
return authorityList;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
/**
* 用户登录的service实现类
* 框架的默认实现是{@link JdbcDaoImpl}
*
* @author ygsama
*/
@Slf4j
@Service("userDetailsService")
public class LoginUserDetailsServiceImpl implements UserDetailsService {
private final AuthUserMapper authUserMapper;
private final AuthRoleMapper authRoleMapper;
@Autowired
public LoginUserDetailsServiceImpl(AuthUserMapper authUserMapper, AuthRoleMapper authRoleMapper) {
this.authUserMapper = authUserMapper;
this.authRoleMapper = authRoleMapper;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUserDO sysUserDO = authUserMapper.selectByPrimaryKey(username);
if (sysUserDO == null) {
throw new UsernameNotFoundException(username);
}
List<SysRoleDO> sysRoles = authRoleMapper.qryUserRoleByUsername(username);
sysUserDO.setRoleList(sysRoles);
log.info("[loadUserByUsername]: {}", sysUserDO);
return sysUserDO;
}
}
/**
* 登录认证的Provider,自定义实现了{@link AuthenticationProvider}
* Provider默认实现是 {@link DaoAuthenticationProvider}
* {@link BearerTokenAuthenticationFilter} 装填=> {@link Authentication}对象
* {@link UsernamePasswordAuthenticationFilter} 调用=> {@link AuthenticationManager} => {@link AuthenticationProvider}验证
*
* @author ygsama
*/
@Slf4j
@Component
public class LoginAuthenticationProvider implements AuthenticationProvider {
private final LoginUserDetailsServiceImpl userDetailService;
private final PasswordEncoder passwordEncoder;
@Autowired
public LoginAuthenticationProvider(LoginUserDetailsServiceImpl userDetailService, PasswordEncoder passwordEncoder) {
this.userDetailService = userDetailService;
this.passwordEncoder = passwordEncoder;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// http请求的账户密码
String username = authentication.getName();
String password = (String) authentication.getCredentials();
// 数据库根据用户名查询
UserDetails userDetails = userDetailService.loadUserByUsername(username);
log.info("[http请求的账户密码]: {}/{}", username, password);
log.info("[数据库查询的账户密码]:{} ", userDetails);
if (userDetails == null) {
throw new BadCredentialsException("用户名未找到");
}
if (!passwordEncoder.matches(password, userDetails.getPassword())) {
throw new BadCredentialsException("密码不正确");
}
Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
log.info("[设置authorities] : {}", authorities);
return new UsernamePasswordAuthenticationToken(userDetails, password, authorities);
}
/**
* 是否支持处理当前Authentication对象类似
*/
@Override
public boolean supports(Class<?> authentication) {
return true;
}
}
SpringSecurity自定义权限验证时也需要实现三个接口:
AccessDecisionManager : 自定义鉴权管理器,根据URL资源权限
和用户角色权限
进行鉴权
FilterInvocationSecurityMetadataSource:自定义权限数据源,提供所有URL资源
与对应角色权限
的映射集合
AbstractSecurityInterceptor :资源访问过滤器,拦截访问请求,封装成安全对象FilterInvocation
,调用前两个实例进行鉴权
/**
* {@link AccessDecisionManager} 鉴权决策管理器
* 被鉴权决策管理器 被{@link AbstractSecurityInterceptor} 调用进行鉴权
* 框架默认实现是 {@link UnanimousBased}
*
* @author ygsama
*/
@Slf4j
@Service
public class AccessDecisionManagerImpl implements AccessDecisionManager {
/**
* 权限鉴定
*
* @param authentication from SecurityContextHolder.getContext() =》 userDetails.getAuthorities()
* @param object 是一个安全对象类型,FilterInvocation.class
* @param configAttributes from MetaDataSource.getAttributes(),已经被框架做了非空判断
*/
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
throws AccessDeniedException, InsufficientAuthenticationException {
log.info("[资源权限]: {}", configAttributes);
log.info("[用户权限]: {}", authentication.getAuthorities());
Iterator<ConfigAttribute> it = configAttributes.iterator();
while (it.hasNext()) {
// 资源的权限
ConfigAttribute resourceAttr = it.next();
String resourceRole = "ROLE_" + ((SecurityConfig) resourceAttr).getAttribute();
// 用户的权限
for (GrantedAuthority userAuth : authentication.getAuthorities()) {
log.info("[资源角色==用户角色] ? {} == {}", resourceRole.trim(), userAuth.getAuthority().trim());
if (resourceRole.trim().equals(userAuth.getAuthority().trim())) {
return;
}
}
}
throw new AccessDeniedException("权限不足");
}
/**
* 被AbstractSecurityInterceptor调用,遍历ConfigAttribute集合,筛选出不支持的attribute
*/
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
/**
* 被AbstractSecurityInterceptor调用,验证AccessDecisionManager是否支持这个安全对象的类型。
*/
@Override
public boolean supports(Class<?> clazz) {
return true;
}
}
/**
* 权限资源映射的数据源
* 这里重写并实现了基于数据库的权限数据源
* 实现了 {@link FilterInvocationSecurityMetadataSource}接口
* 框架的默认实现是 {@link DefaultFilterInvocationSecurityMetadataSource}
*
* @author ygsama
*/
@Slf4j
@Service
public class AccessSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
// key 是url+method , value 是对应url资源的角色列表
private static Map<RequestMatcher, Collection<ConfigAttribute>> permissionMap;
// 从数据库取出权限数据,代码略
private AuthRoleMapper authRoleMapper;
private AuthPermissionMapper authPermissionMapper;
@Autowired
AccessSecurityMetadataSource(AuthRoleMapper authRoleMapper, AuthPermissionMapper authPermissionMapper) {
this.authPermissionMapper = authPermissionMapper;
this.authRoleMapper = authRoleMapper;
}
/**
* 在Web服务器启动时,缓存系统中的所有权限映射。
* 被{@link PostConstruct}修饰的方法会在服务器加载Servlet的时候运行(构造器之后,init()之前)
*/
@PostConstruct
private void loadResourceDefine() {
permissionMap = new LinkedHashMap<>();
// 需要鉴权的url资源,@needAuth标志
List<SysPermissionDO> permissionList = authPermissionMapper.queryRolePermission();
for (SysPermissionDO permission : permissionList) {
String url = permission.getUrl();
String method = permission.getMethod();
String[] roles = permission.getRoleList().split(",");
log.info("{} - {}", url, method);
AntPathRequestMatcher requestMatcher = new AntPathRequestMatcher(url, method);
Collection<ConfigAttribute> attributes = new ArrayList<>();
for (String role : roles) {
attributes.add(new SecurityConfig(role));
}
// 占位符,需要权限才能访问的资源 都需要添加一个占位符,保证value不是空的
attributes.add(new SecurityConfig("@needAuth"));
permissionMap.put(requestMatcher, attributes);
}
// 公共的url资源 & 系统接口的url资源,value为null
List<SysPermissionDO> publicList = authPermissionMapper.queryPublicPermission();
for (SysPermissionDO permission : publicList) {
String url = permission.getUrl();
String method = permission.getMethod();
AntPathRequestMatcher requestMatcher = new AntPathRequestMatcher(url, "*".equals(method) ? null : method);
// value为空时不做鉴权,相当于所有人都可以访问该资源URL
permissionMap.put(requestMatcher, null);
}
// 多余的url资源, @noAuth,所有人都无法访问
Collection<ConfigAttribute> attributes = new ArrayList<>();
attributes.add(new SecurityConfig("@noAuth"));
permissionMap.put(new AntPathRequestMatcher("/**", null), attributes);
log.info("[全局权限映射集合初始化]: {}", permissionMap.toString());
}
/**
* 鉴权时会被AbstractSecurityInterceptor.beforeInvocation()调用,根据URL找到对应需要的权限
*
* @param object 安全对象类型 FilterInvocation.class
*/
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
log.info("[资源被访问:根据URL找到权限配置]: {}\n {}", object, permissionMap);
if (permissionMap == null) {
loadResourceDefine();
}
final HttpServletRequest request = ((FilterInvocation) object).getRequest();
for (Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : permissionMap.entrySet()) {
if (entry.getKey().matches(request)) {
log.info("[找到的Key]: {}", entry.getKey());
log.info("[找到的Value]: {}", entry.getValue());
if (entry.getValue().size() > 0) {
return entry.getValue();
}
}
}
return null;
}
/**
* 用于被AbstractSecurityInterceptor调用,返回所有的 Collection ,以筛选出不符合要求的attribute
*/
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return new ArrayList<>();
}
/**
* 用于被AbstractSecurityInterceptor调用,验证指定的安全对象类型是否被MetadataSource支持
*/
@Override
public boolean supports(Class<?> clazz) {
return true;
}
}
/**
* 资源访问过滤器
* 重写了{@link AbstractSecurityInterceptor} 接口
* 默认的过滤器实现是{@link FilterSecurityInterceptor}
*
* @author ygsama
*/
@Slf4j
@Component
public class AccessSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
// 注入前面的两个实例
private final AccessSecurityMetadataSource accessSecurityMetadataSource;
private final AccessDecisionManagerImpl accessDecisionManagerImpl;
@Autowired
public AccessSecurityInterceptor(AccessSecurityMetadataSource accessSecurityMetadataSource, AccessDecisionManagerImpl accessDecisionManagerImpl) {
this.accessSecurityMetadataSource = accessSecurityMetadataSource;
this.accessDecisionManagerImpl = accessDecisionManagerImpl;
}
/**
* 初始化时将自定义的DecisionManager,注入到父类AbstractSecurityInterceptor中
*/
@PostConstruct
public void initSetManager() {
super.setAccessDecisionManager(accessDecisionManagerImpl);
}
/**
* 重写父类AbstractSecurityInterceptor,获取到自定义MetadataSource的方法
*/
@Override
public SecurityMetadataSource obtainSecurityMetadataSource() {
return this.accessSecurityMetadataSource;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("[自定义过滤器]: {}", " LoginSecurityInterceptor.doFilter()");
FilterInvocation fi = new FilterInvocation(request, response, chain);
// 调用父类的 beforeInvocation ==> accessDecisionManager.decide(..)
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
// 调用父类的 afterInvocation ==> afterInvocationManager.decide(..)
super.afterInvocation(token, null);
}
}
/**
* 向父类提供要处理的安全对象类型,因为父类被调用的方法参数类型大多是Object,框架需要保证传递进去的安全对象类型相同
*/
@Override
public Class<?> getSecureObjectClass() {
return FilterInvocation.class;
}
@Override
public void init(FilterConfig filterConfig) {
}
@Override
public void destroy() {
}
}
WebSecurityConfigurerAdapter 继承后将上面自定义的一些配置添加到框架中
如果是OAuth2 认证还需要继承实现以下两个配置类:
ResourceServerConfigurerAdapter
AuthorizationServerConfigurerAdapter
此处省略,详见源码:https://github.com/ygsama/ipa