分析项目
mall-tiny-04
流转流程
1、访问登录接口
访问接口/admin/login
来登录,由于该项目添加了SpringSecurity
配置,所以我们来看下com.macro.mall.tiny.config.SecurityConfig
,里面配置了对/admin/login
接口是不需要认证,如下:
并且在com.macro.mall.tiny.controller.UmsAdminController》login
方法上也没有添加@PreAuthorize
,所以也不需要权限限制,既然认证授权都不需要,那就直接执行该方法即可
2、获取token返回前端
在上述的login方法中,我们可以看到以下代码:
String token = adminService.login(umsAdminLoginParam.getUsername(), umsAdminLoginParam.getPassword());
我们先来看该login方法中的第一行代码,如下:
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
由于在com.macro.mall.tiny.config.SecurityConfig
中重新定义了一个UserDetailsService
,这就是上述userDetailsService
对应的Bean
public UserDetailsService userDetailsService() {
return new UserDetailsService() {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UmsAdmin admin = adminService.getAdminByUsername(username);
if (admin != null) {
List<UmsPermission> permissionList = adminService.getPermissionList(admin.getId());
// 取自AuthenticationException
return new AdminUserDetails(admin,permissionList);
}
// 取自AuthenticationException
throw new UsernameNotFoundException("用户名或密码错误");
}
};
}
“mall作者”中是lambda表达式写法,这上面是匿名内部类写法,因此userDetailsService.loadUserByUsername(username)
中调用的就是上面匿名内部类中的写法,我们来看匿名内部类中的第一行代码UmsAdmin admin = adminService.getAdminByUsername(username);
,其实这行代码的作用就是根据用户名获取用户详细信息对象,当然我们还可以看getAdminByUsername()方法的具体实现,大家可能对example.createCriteria().andUsernameEqualTo(username);
有所困惑,其实该代码的含义就是为sql语句拼接做准备,大家可以看一下UmsAdminMapper.xml
中id=selectByExample
的sql语句,其实<include refid="Example_Where_Clause" />
作用就是拼接where username = "XXX"
,现在我们就从数据库中获取到用户详细信息了
之后根据用户id使用List<UmsPermission> permissionList = adminService.getPermissionList(admin.getId());
从数据库中取出了用户的权限信息
最后通过return new AdminUserDetails(admin,permissionList);
返回了用户信息和权限信息
当然AdminUserDetails
实现了UserDetails
,所以UserDetails userDetails = userDetailsService.loadUserByUsername(username);
中才能使用UserDetails
来接收loadUserByUsername
方法的返回值
我们继续回到com.macro.mall.tiny.service.impl.UmsAdminServiceImpl》login
方法,现在我们已经获得了userDetails
对象,里面有用户信息和权限信息,之后我们通过以下代码来验证密码是否正确,如下:
if (!passwordEncoder.matches(password, userDetails.getPassword())) {
throw new BadCredentialsException("密码不正确");
}
之后把权限赋值给了用户,但是我感觉这个授权没啥用,毕竟直接把token返回了,权限也就在过滤器链中有效,如下:
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
之后通过工具类获取了token,如下:
token = jwtTokenUtil.generateToken(userDetails);
之后com.macro.mall.tiny.controller.UmsAdminController》login
方法中就把token放在了Map集合中返回给前端
3、获取品牌列表
调用/brand/listAll
接口,由于SpringSecurity的存在,所以一定会经过com.macro.mall.tiny.config.SecurityConfig
,里面的configure
方法会拦截该请求,然后交给httpSecurity.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
中的JwtAuthenticationTokenFilter
对象处理,我们来看JwtAuthenticationTokenFilter
的内部信息,里面有一个doFilterInternal
方法,我们来具体看一下com.macro.mall.tiny.component.JwtAuthenticationTokenFilter》doFilterInternal
方法的详细代码,如下:
第一行是String authHeader = request.getHeader(this.tokenHeader);
,作用是获取header中的token,由于token以Bearer
开头,所以就有了下述两条语句,如下:
if (authHeader != null && authHeader.startsWith(this.tokenHead)) {
String authToken = authHeader.substring(this.tokenHead.length());
……
}
之后我们从token中获取username,如下:
String username = jwtTokenUtil.getUserNameFromToken(authToken);
getUserNameFromToken
方法是用来获取username的,如果token有误或者token过期都会导致username是null,如果username是null,那么SecurityContextHolder.getContext().getAuthentication()
的值就是null,那么在访问接口的时候就会抛出AuthenticationException
异常,然后根据以下代码可以知道抛出异常的时候会交给restAuthenticationEntryPoint
对象,如下:
httpSecurity.exceptionHandling()
.accessDeniedHandler(restfulAccessDeniedHandler)
.authenticationEntryPoint(restAuthenticationEntryPoint);
大家可以看com.macro.mall.tiny.component.RestAuthenticationEntryPoint
类里面的commence
方法就会返回值给前端
我们可以继续回到com.macro.mall.tiny.component.JwtAuthenticationTokenFilter
类,假设已经通过String username = jwtTokenUtil.getUserNameFromToken(authToken);
代码取到了username
,此时if (username != null && SecurityContextHolder.getContext().getAuthentication() == null)
中的条件满足要求,通过UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
代码可以获取到userDetails
其中的loadUserByUsername
方法调用的还是com.macro.mall.tiny.config.SecurityConfig#userDetailsService
中的loadUserByUsername
方法,上面已经分析过了,这里不再分析,之后又调用了if (jwtTokenUtil.validateToken(authToken, userDetails))
,感觉这个调用没啥用,因为if中内容的作用是验证username是否正确和token是否过期,首先username来自于token,这肯定是一样的,另外token如果过期,那么username是null,然后就没法到这个if判断,所以我说这个判断很鸡肋,不知道你是否担心UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
获取的userDetails是null,其实不会的,因为如果无法获取到用户对象,早已经抛出了异常的,之后就是授权操作了,如果有相应的权限才可以调用相应的方法
如果用户有相应的权限值,那就可以通过@PreAuthorize("hasAuthority('pms:brand:read')")
中的权限鉴定,如果无法通过权限鉴定,将会抛出AccessDeniedException
异常,然后我们在com.macro.mall.tiny.config.SecurityConfig》configure
中定义了如下以下处理器:
httpSecurity.exceptionHandling()
.accessDeniedHandler(restfulAccessDeniedHandler)
.authenticationEntryPoint(restAuthenticationEntryPoint);
本次由restfulAccessDeniedHandler
对象处理该异常,你也可以看到它会将某些内容返回到前端
4、在没有登录的情况下,无法访问没有权限注解的方法
估计是SecurityContextHolder.getContext().getAuthentication()
为null,在调用到方法之前被SpringSecurity发现根本没有经过认证,所以触发了AuthenticationException
异常,然后就根据com.macro.mall.tiny.component.RestAuthenticationEntryPoint》commence
方法中的返回结果给前端做了返回,不过这也只是猜测,但是验证结果是未登录无法访问接口