这里对SpringSecurity进行一个简单的整理。
从参考来看,SpringSecurity是责任链模式,先认证(authentication),再授权(Authorization)。
changeLog:
简介
参与要素
- username用户
- password密码
- role(或者permission)
- url
简介
认证:用户登录时,使用username与password登录,系统进行认证,认证通过之后,将user信息通过session保存
授权:在访问访问url时,系统根据url查找对应的role(或permission),然后再查看该用户是否具备该role(permission)
认证(authentication)
逻辑
登录
- 用户登录,携带username + password
- 后端根据username从数据库获取user信息,尤其是password
- 对比password,相同则登录成功,生成token,并缓存信息;反之则认证失败
认证
- 访问其他路由时,应携带token信息
- 通过拦截器(或过滤器)拦截request请求,获取其中的token,没有返回401
- 与查询缓存,是否有该token,没有则401,有则放行
流程
SpringSecurtity自己封装了login路由与对应的页面,它的过程如下:
-
用户登录被AbstractAuthenticationProcessingFilter(UsernamePasswordAuthenticationFilter)拦截
将username与password封装成Authentication对象
-
将该Authentication对象传递给AuthenticationManager(chain)进行验证
AuthenticationManager是接口,其实现是ProviderManager
-
ProviderManager调用AuthenticationProvider(handler)获取用户信息
在DaoAuthenticationProvider(AuthenticationProvider的实现)中调用UserDetailsService去获取用户UserDetail,并进行对比。
-
验证成功返回一个封装了权限信息(role或permission)的Authentication对象
此时的Authentication包括着用户名、密码、权限信息
-
SpringSecurtiy框架将该对象放到SecurityContext中,使用时可以从SecurityContextHolder上下文中取出。
【缺少认证部分】
类
代码
登录拦截
在UsernamePasswordAuthenticationFilter中进行了拦截,通过attemptAuthentication进行处理
public class UsernamePasswordAuthenticationFilter extends
AbstractAuthenticationProcessingFilter {
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
private boolean postOnly = true;
// 构造器,以不区分大小写的方式post方式和HTTP方法创建匹配器。
public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
// 从请求路径获取用户名和密码
String username = obtainUsername(request);
String password = obtainPassword(request);
// 空值判断
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
// 去除用户名首尾空格
username = username.trim();
// 生成一个用户名密码身份验证的令牌
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
// 设置身份认证请求的信息
setDetails(request, authRequest);
// 返回一个完全经过身份验证的对象,包括凭据
return this.getAuthenticationManager().authenticate(authRequest);
}
....
protected String obtainUsername(HttpServletRequest request) {
return request.getParameter(usernameParameter);
}
}
获取用户
这里还有一个UserDetailsService类,是框架用来获取用户信息的类,其只有一个接口loadUserByUsername
,通过用户名获取用户的详细信息。
@Service
public class MyUserDetailsService implements UserDetailsService {
@Autowired
MyUserMapper mapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
MyUserBean userBean = mapper.selectByUsername(username);
if (userBean == null) {
throw new UsernameNotFoundException("数据库中无此用户!");
}
return userBean;
}
}
通过AuthenticationProvider与UserDetailsService都通过SpringConfiguration放到配置中
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
BackdoorAuthenticationProvider backdoorAuthenticationProvider;
@Autowired
MyAccessDeniedHandler myAccessDeniedHandler;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
...省略
//将自定义验证类注册进去
auth.authenticationProvider(backdoorAuthenticationProvider);
auth.userDetailsService(myUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}
...省略
}
密码验证
这里稍微有点疑问,从代码看,可以通过AuthenticationProvider手动写代码进行验证,如果通过返回Authentication类,否则不返回;而这里添加的UserDetailsService是根据用户名获取数据的,显然也是为了对比密码做准备的。疑问是,密码对比到底在哪进行?
通过断点发现,UserDetailService 是由DaoAuthenticationProvider调用,而它是AbstractUserDetailsAuthenticationProvider的一个子类,在AbstractUserDetailsAuthenticationProvider::authenticate中,调用了子类的DaoAuthenticationProvider::retrieveUser()函数,由此获取用户的信息,然后调用了additionalAuthenticationChecks来实现,而这个方法是有DaoAuthenticationProvider实现的。
这是典型的template模式。
看看这段代码。
class AbstractUserDetailsAuthenticationProvider{
public Authentication authenticate(Authentication authentication){
//...省略
// 获取用户,若不存在,则抛异常
try {
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException notFound) {
//..省略
throw notFound;
}
//...省略
//additionalAuthenticationChecks,将用户信息与anthentication传入,进行验证。
try {
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException exception) {
//...省略
throw exception;
}
// ...省略
}
// ...省略
this.createSuccessAuthentication(principalToReturn, authentication, user);
}
在来看看密码验证的实现:
class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
private PasswordEncoder passwordEncoder;
private UserDetailsService userDetailsService;
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
//...省略
String presentedPassword = authentication.getCredentials().toString();
if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
}
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
//...省略
}
}
示例
通过Provider增加admin后门
业务逻辑基本都是在AuthenticationProvider中实现,然后通过Security的配置,加入到验证链中。
@Component
public class BackdoorAuthenticationProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String name = authentication.getName();
String password = authentication.getCredentials().toString();
//利用alex用户名登录,不管密码是什么都可以,伪装成admin用户
if (name.equals("admin")) {
Collection<GrantedAuthority> authorityCollection = new ArrayList<>();
authorityCollection.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
authorityCollection.add(new SimpleGrantedAuthority("ROLE_USER"));
return new UsernamePasswordAuthenticationToken(
"admin", password, authorityCollection);
} else {
return null;
}
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(
UsernamePasswordAuthenticationToken.class);
}
}
绕过自带login的方法
【PS:对于使用security这种方法麻烦】
-
如果要绕过自带的login,需要自己实现login路由,login成功之后,生成token并缓存。因为login自己实现了,所以框架主要做的就是认证部分。
-
重实现AbstractAuthenticationProcessingFilter来取代框架自带的UsernamePasswordAuthenticationFilter.封装auth对象时,仅存token即可
ublic class TokenAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private static final String BEARER = "Bearer";
public TokenAuthenticationFilter(final RequestMatcher requiresAuth) {
super(requiresAuth);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
final String param = Optional.ofNullable(request.getHeader(AUTHORIZATION)).orElse(request.getParameter("t"));
final String token = Optional.ofNullable(param)
// .map(value-> removeStart(value, BEARER))
.map(String::trim)
.orElseThrow(()->new BadCredentialsException("Missing Authentication Token"));
Authentication auth = new UsernamePasswordAuthenticationToken(token, token);
return getAuthenticationManager().authenticate(auth);
}
...
}
-
实现AbstractUserDetailsAuthenticationProvider来获取缓存中的用户
tokenService是从redis缓存中通过token去获取用户
public class TokenAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
@Autowired
TokenService tokenService;
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
}
@Override
protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
final Object token = authentication.getPrincipal();
return Optional
.ofNullable(token)
.map(String::valueOf)
.flatMap(tokenService::getUserByToken)
.orElseThrow(()->new UsernameNotFoundException("Cannot find user with authentication token=" + token));
}
授权(authorization)
逻辑
授权是在认证的基础之上的
流程
-
访问url时,会通过AbstractSecurityInterceptor拦截器拦截
-
它会调用FilterInvocationSecurityMetadataSource的方法来获取被拦截url所需的全部权限
-
然后调用授权管理器AccessDecisionManager来进行验证
这个授权管理器会通过spring的全局缓存SecurityContextHolder获取用户的权限信息,还会获取被拦截的url和被拦截url所需的全部权限,然后根据所配的策略(有:一票决定,一票否定,少数服从多数等)进行验证。如果权限足够,则返回,权限不够则报错。
类
这里有点模板模式的意思,但更复杂一些。
调用的入口应该是FilterSecurityIntercepor的invoke()函数,在这个函数中,调用了父函数的beforeInvocation()、finallyInvocation()、afterInvocation()。对授权的处理,就在AbstractSecurityInterceptor::beforeInvocation()中实现的。
在这个函数中,通过调用obtainSecurityMetadataSource()获取了FilterSecurityInterceptor中的对象(自己实现的),通过它获取了与url匹配权限。然后将数据交给了AccessDesionManager(自己实现)的decide()函数判断是否通过。
代码
【缺少后2部分的源码】
AbstractSecurityInterceptor
AbstractSecurityInterceptor::beforeInvocation中的调用情况
protected InterceptorStatusToken beforeInvocation(Object object) {
...
// 调用了MySecurityMetadataSource
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
.getAttributes(object);
// 获取用户的Authentication
Authentication authenticated = authenticateIfRequired();
// 调用decide
try {
this.accessDecisionManager.decide(authenticated, object, attributes);
}
catch (AccessDeniedException accessDeniedException) {
publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
accessDeniedException));
throw accessDeniedException;
}
....
}
通过url获取所需要的权限
@Component
public class MySecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
@Autowired
ResourceMapper resourceMapper;
//本方法返回访问资源所需的角色集合
AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
//从object中得到需要访问的资源,即网址
String requestUrl = ((FilterInvocation) object).getRequestUrl();
//从数据库中得到所有资源,以及对应的角色
List<MyResourceBean> resourceBeans = resourceMapper.selectAllResource();
for (MyResourceBean resource : resourceBeans) {
//首先进行地址匹配
if (antPathMatcher.match(resource.getUrl(), requestUrl)
&& resource.getRolesArray().length > 0) {
return SecurityConfig.createList(resource.getRolesArray());
}
}
//匹配不成功返回一个特殊的ROLE_NONE
return SecurityConfig.createList("ROLE_NONE");
}
...
}
通过AccessDecisionManager鉴权
看看decide
@Component
public class MyAccessDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
throws AccessDeniedException, InsufficientAuthenticationException {
//从authentication中获取当前用户具有的角色
Collection<? extends GrantedAuthority> userAuthorities = authentication.getAuthorities();
//从configAttributes中获取访问资源所需要的角色,它来自MySecurityMetadataSource的getAttributes
Iterator<ConfigAttribute> iterator = configAttributes.iterator();
while (iterator.hasNext()) {
ConfigAttribute attribute = iterator.next();
String role = attribute.getAttribute();
if ("ROLE_NONE".equals(role)) {
if (authentication instanceof AnonymousAuthenticationToken) {
throw new BadCredentialsException("用户未登录");
} else
return;
}
//逐一进行角色匹配
for (GrantedAuthority authority : userAuthorities) {
if (authority.getAuthority().equals("ROLE_ADMIN")) {
return; //用户具有ROLE_ADMIN权限,则可以访问所有资源
}
if (authority.getAuthority().equals(role)) {
return; //匹配成功就直接返回
}
}
}
//不能完成匹配
throw new AccessDeniedException("你没有访问" + object + "的权限!");
}
}
示例
注解方式明确路由的权限
按照security的使用方式,授权被框架封装起来了,只需要在路由上增加注解,说明需要的权限即可。
@ApiOperation("查询部门")
@GetMapping("/api/dept/{id}")
@PreAuthorize("@el.check('user:list','admin','dept:detail')")
public ResponseEntity<Object> getDeptById(@PathVariable Long id) {
...
}
这个el.check()如下:
@Service(value = "el")
public class ElPermissionConfig {
public Boolean check(String... permissions) {
// 获取当前用户的所有权限
List<String> elPermissions = SecurityUtils.getUserDetails().getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());
// 判断当前用户的所有权限是否包含接口上定义的权限
return elPermissions.contains("admin") || Arrays.stream(permissions).anyMatch(elPermissions::contains);
}
}
不在需要FilterInvocationSecurityMetadataSource与AccessDecisionManager的内容。
这种具体的实现方式待实践。
数据库方式
如果需要路由与权限的信息存储到数据库中,重新实现FilterInvocationSecurityMetadataSource、AccessDecisionManager
通过实现FilterInvocationSecurityMetadataSource从数据库中读取权限
@Component
public class SecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
@Autowired
SourceService sourceService;
AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
String requestUrl = ((FilterInvocation) object).getRequestUrl();
String method = ((FilterInvocation) object).getRequest().getMethod();
List<Source> sourceList = sourceService.findSourceByMethod(method);
List<String> permissionList = new ArrayList<>();
for (Source source: sourceList) {
if(antPathMatcher.match(source.getUrl(), requestUrl) && !source.getPermissions().isEmpty()){
permissionList.add(source.getPermissions());
}
}
// if(permissionList.size() == 0){
// permissionList.add("NONE");
// }
String[] str = new String[permissionList.size()];
for (int i = 0; i < permissionList.size(); i++) {
str[i] = permissionList.get(i);
}
return SecurityConfig.createList(str);
}
...
}
通过实现AccessDecisionManager来进行鉴权
public class MyAccessDecisionManager implements AccessDecisionManager {
@Autowired
TokenService tokenService;
@Override
public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
User user = (User)authentication.getPrincipal();
Iterator<ConfigAttribute> iterator = collection.iterator();
List<String> permissions = new ArrayList<>();
while (iterator.hasNext()){
ConfigAttribute attribute = iterator.next();
permissions.addAll(Arrays.asList(attribute.getAttribute().split(";")));
}
if(permissions.size() == 0){
return;
}
for (String userPermission: user.getPermissionList()) {
if(permissions.contains(userPermission)){
return;
}
}
throw new AccessDeniedException("你没有访问权限!");
}
这两个类也是通过SecurityConfiguration配置进了框架
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
BackdoorAuthenticationProvider backdoorAuthenticationProvider;
@Autowired
MyAccessDeniedHandler myAccessDeniedHandler;
@Autowired
MyAccessDecisionManager myAccessDecisionManager;
@Autowired
MySecurityMetadataSource mySecurityMetadataSource;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
...省略
//将自定义验证类注册进去
auth.authenticationProvider(backdoorAuthenticationProvider);
auth.userDetailsService(myUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
object.setSecurityMetadataSource(mySecurityMetadataSource);
object.setAccessDecisionManager(myAccessDecisionManager);
return object;
}
})
.and()
.csrf().disable()
.exceptionHandling().accessDeniedHandler(myAccessDeniedHandler);
}
...省略
}
【SecurityContext存在哪里?】