总结来源:b站up三更
整个security的原理其实就是一个过滤器链,内部包含了各种功能的过滤器,其中比较核心的就是关于认证的过滤器、异常处理的过滤器和关于授权的过滤器
1.关于认证
图源自:b站up主三更
首先,当请求带着身份信息进来,用户名通常被UsernamePasswordAuthenticationToken这个实现类封装成authentication
UsernamePasswordAuthenticationToken authentication
= new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());
然后,注入AuthenticationManager对象调用authenticate方法去认证并返回一个充满权限信息身份信息和细节信息的Authentication实例
Authentication authenticate =
authenticationManager.authenticate(authenticationToken);
这个时候,SecurityContextHolder的上下文容器就已经有个充满信息的Authentication,而调用authenticate方法的实际是AuthenticationManager的实现类ProviderManager的方法,然后在后面会自动去调用AbstractUserDetailsAuthenticationProvider的实现类的DaoAuthenticationProvider的authenticate()方法,这里所需要数据库的信息,框架会再次自动调用UserDetailsService的子类InMemoryUserDetailsManager的loadUserByUsername方法去数据库查询,但是为了个性化定制查询规则我们会自定义一个UserDetailsServiceImpl实现类去实现UserDtailsService接口的loadUserByUsername方法,然后根据自己想要的规则重写该方法去查询数据库什么的,再将查询到的用户信息和权限信息封装成一个UserDtails对象返回上去,而这个我们往往自定义一个UserDetails的实现类,成员变量往往是用户信息和权限信息,还要重写getAuthorities的方法,方法里面其中要将string类型的权限信息封装成SimpleGrantedAuthority对象并且要后面所有的重写的方法放行
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if(authorities!=null){
return authorities;
}
//把permission中String类型的权限信息封装成SimpleGrantedAuthority对象
/*ArrayList<GrantedAuthority> newList = new ArrayList<>();
for (String permission : permissions) {
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permission);
newList.add(authority);
}*/
//把permissions中字符串类型的权限信息转换成GrantedAuthority对象存入authority中
authorities = permissions.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
return authorities;
}
返回UserDetails对象给了DaoAuthenticationProvider的authenticate方法去认证,这里会通过PasswordEncoder对比UserDetails方法中的密码和Authtication中的密码,如果正确就会把UserDetails中的权限信息设置到Authentication对象中,再将authentication返回,返回之后将authentication设置到SecurityContextHolder的context里面
但是如果我们每次请求都去查询数据库,会造成比较大的压力,我们需要设置一个认证过滤器。我们第一次登录成功时,可以根据userid用JWT生成一个token,并将token返回给前端存入请求头里面,同时,我们也会将authentication的信息存入到reids中键设置为"login"+userid,值设置为我们返回的userDetail对象
下次请求发过来再让我们过滤器先去过滤,过滤器中会去获取token,解析token从中取出userid,然后再用userid去redis中查询对应的用户信息,也就是userDetails对象,这里也就是说不需要再去数据库查询权限信息,直接将userDetails封装成一个authentication即可,然后再将authentication设置到SecurityContextHolder的context里面
上述就是大致的认证流程
关于数据库的密码加密
实际项目中我们不会把密码明文存储在数据库中。 默认使用的PasswordEncoder要求数据库中的密码格式为:{id}password 。它会根据id去判断密码的 加密方式。但是我们一般不会采用这种方式。所以就需要替换PasswordEncoder。 我们一般使用SpringSecurity为我们提供的BCryptPasswordEncoder。 我们只需要使用把BCryptPasswordEncoder对象注入Spring容器中,SpringSecurity就会使用该 PasswordEncoder来进行密码校验。 我们可以定义一个SpringSecurity的配置类,SpringSecurity要求这个配置类要继承 WebSecurityConfigurerAdapter。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//关闭csrf
.csrf().disable()
//不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 对于登录接口 允许匿名访问
.antMatchers("/user/login").anonymous()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
}
}
注意:上面的所需要用到的AuthenticationManager也要注入在配置类中,这个配置类继承了WebSecurityConfigurerAdapter,需要重写configure方法对发来的请求做出处理
关于授权:
我们往往会用FilterSecurityInterceptor进行权限校验,从SecurityContextHolder中取出封装好的Authentication,然后获取权限信息,再根据判断用户是否拥有该资源的访问权限
大致的操作顺序为:
- 在配置类开启配置
@EnableGlobalMethodSecurity(prePostEnabled = true)
- 在需要访问的资源上加上注解
注意:这个hasAuthority其实是可以自己定义的 可以自己写一个类然后写一个hasAuthority方法 并在@conponent里面写一个别名 然后再@PreAuthority用SPEL表达式 比如@PreAuthorize("hasAuthority('test')")
@PreAuthorize("@ex.hasAuthority('权限名字符串')") @Component("ex") public class SGExpressionRoot { public boolean hasAuthority(String authority){ //获取当前用户的权限 Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); LoginUser loginUser = (LoginUser) authentication.getPrincipal(); List<String> permissions = loginUser.getPermissions(); //判断用户权限集合中是否存在authority return permissions.contains(authority); } }
- 建立RBAC模型 : 其实也就是五张表 用户表 用户角色表 角色表 角色权限表 权限表
- sql查询大致就是通过userid查询用户对应的角色,再一个左外连根据userid对应的角色id对应的权限id查询到权限id,再根据权限id查询到权限
<select id="selectPermsByUserId" resultType="java.lang.String">
SELECT
DISTINCT m.`perms`
FROM
sys_user_role ur
LEFT JOIN `sys_role` r ON ur.`role_id` = r.`id`
LEFT JOIN `sys_role_menu` rm ON ur.`role_id` = rm.`role_id`
LEFT JOIN `sys_menu` m ON m.`id` = rm.`menu_id`
WHERE
user_id = #{userid}
AND r.`status` = 0
AND m.`status` = 0
</select>
4.然后在配置类的config方法中添加这过滤器
//添加过滤器
http.
addFilterBefore(jwtAuthenticationTokenFilter,
UsernamePasswordAuthenticationFilter.class);
关于异常处理
我们可以自定义实现类去自定义处理方法比如
授权过程异常处理
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException)
throws IOException,
ServletException {
ResponseResult result = new ResponseResult(HttpStatus.FORBIDDEN.value(),"权限不足");
String json = JSON.toJSONString(result);
WebUtils.renderString(response,json);
}
}
认证过程异常处理
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponseresponse, AuthenticationException authException) throws IOException,ServletException {
ResponseResult result = new
ResponseResult(HttpStatus.UNAUTHORIZED.value(), "认证失败请重新登录");
String json = JSON.toJSONString(result);
WebUtils.renderString(response,json);
}
}
再配置给处理器
@Autowired
private AuthenticationEntryPoint authenticationEntryPoint;
@Autowired
private AccessDeniedHandler accessDeniedHandler;
再给config对象添加处理器
http.exceptionHandling().
authenticationEntryPoint(authenticationEntryPoint).
accessDeniedHandler(accessDeniedHandler);
其他问题详情请参考b站up三更