1.认证
密码校验用户
密码加密存储
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Beanpublic PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}}
- 我们没有以上代码配置,默认明文存储, {id}password;
- 实现这个将passworxEncode变成@Bean之后,就变成加密存储: 加盐值 + 明文随机生成一段密文;
- 将返回UserDetails对象中的密码,与输入的密码进行校验
登录接口
- 在SpringSecurity放行登录接口,
- 定义AuthenticationManager @bean
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Beanpublic PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}@Overrideprotected void configure(HttpSecurity http) throws Exception {http//关闭csrf.csrf().disable()//不通过Session获取SecurityContext.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()// 对于登录接口 允许匿名访问.antMatchers("/user/login").anonymous()// 除上面外的所有请求全部需要鉴权认证.anyRequest().authenticated();}@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}}
PS:就我们研究框架, 有些值,需要我们利用断点调试,层层拨开来进行获取。
我们创建一个自定义登录接口,并把内部逻辑交给自定义UserService的实现类的方法来处理。
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {@Autowiredprivate AuthenticationManager authenticationManager;@Autowiredprivate RedisCache redisCache;@Overridepublic ResponseResult login(UserDTO userDTO) {// AuthenticationManager authenticationManager 进行认证UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDTO.getUsername(), userDTO.getPassword());Authentication authenticate = authenticationManager.authenticate(authenticationToken);// 如果认证通过、给出对应提示if(Objects.isNull(authenticate)){throw new RuntimeException("登录失败");}// 认证通过使用userid生成jwtLoginUser loginUser = (LoginUser) authenticate.getPrincipal();String userId = loginUser.getUser().getId().toString();String token = JwtUtil.createJWT(userId);HashMap<String, String> map = new HashMap<>();map.put("token",token);// 把完整用户信息存入redisredisCache.setCacheObject("login:"+userId,loginUser);return new ResponseResult(200,"登录成功",map);}
}
认证过滤器
@Component
public class JwtFilter extends OncePerRequestFilter {@Autowiredprivate RedisCache redisCache;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {//获取tokenString token = request.getHeader("Authorization").substring(6);if(StringUtils.isEmpty(token)){filterChain.doFilter(request,response);return;}//解析tokenString userid;try {Claims claims = JwtUtil.parseJWT(token);userid = claims.getSubject();} catch (Exception e) {e.printStackTrace();throw new RuntimeException("token非法");}//从redis中获取用户信息String redisKey = "login:" + userid;LoginUser loginUser = redisCache.getCacheObject(redisKey);if(Objects.isNull(loginUser)){throw new RuntimeException("用户未登录");}//存入SecurityContextHolder//TODO 获取权限信息封装到Authentication中UsernamePasswordAuthenticationToken authenticationToken =new UsernamePasswordAuthenticationToken(loginUser,null,loginUser.getAuthorities());SecurityContextHolder.getContext().setAuthentication(authenticationToken);//放行filterChain.doFilter(request, response);}
}
由上面配置,我们得知:
- 假如没有token,token过滤器放行,但是还会经理SpringSecurity过滤器链,由于SecurityContextHolder没有用户信息,过滤器链判断未认证;
- login接口,tokne过滤器也是没有toke 放行,但是我们SpringSecurity过滤器链是默认放行的。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate JwtFilter jwtAuthenticationTokenFilter;@Beanpublic PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}@Overrideprotected void configure(HttpSecurity http) throws Exception {http//关闭csrf.csrf().disable()//不通过Session获取SecurityContext.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()// 对于登录接口 允许匿名访问.antMatchers("/user/login").anonymous()// 除上面外的所有请求全部需要鉴权认证.anyRequest().authenticated();//把token校验过滤器添加到过滤器链中,就是在 UsernamePasswordAuthenticationFilter之前http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);}@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}}
退出接口
- 对于每次请求都是不同的线程。
- 同一个运行程序,一般的静态变量可以被不同线程访问;
- ThreadLocal的静态变量,是线程隔离,只有当前线程才能访问;
- 因此,每次访问的SecurityContextHolder默认是空的,只有经过jwt过滤器之后,变得有意义;
2.授权
我们前面是基于认证的流程,但是实际上,我们认证之后,不同等级用户,需要不同的权限访问。
- 从SpringSecurityInterceptor进行权限校验,那么我们要配置让他权限校验。
限制接口访问权限
springSecurity开启权限配置,1.注解形式 2.配置文件(java配置类)形式
封装权限信息
UserDetailService修改:
UserDetails修改:
@Data
@NoArgsConstructor
public class LoginUser implements UserDetails {private User user;private List<String> authList;@JSONField(serialize = false)private List<SimpleGrantedAuthority> authorities; // SimpleGrantedAuthority对象不支持序列化,无法存入redispublic LoginUser(User user, List<String> authList) { // 将对应的权限字符串列表传入this.user = user;this.authList = authList;}@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {// 初始化之后,我们后续其他拦截器,也会获取; 没必要多次初始化;if(authorities != null){return authorities;}else{authorities = new ArrayList<>();}// 第一次登录,封装UserDetails对象,初始化权限列表for (String auth : authList) {SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(auth);authorities.add(simpleGrantedAuthority); // 对,默认是个空的}return authorities;}@Overridepublic String getPassword() {return user.getPassword();}@Overridepublic String getUsername() {return user.getUserName();}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return true;}
}
认证过滤器更改:将权限信息增加到contex中,以便后续拦截器链获取。
从数据库查询信息
RBAC权限模型
RBAC权限模型(Role-Based Access Control)即:基于角色的权限控制。这是目前最常被开发者使用也是相对易用、通用权限模型。
涉及到的sql语句
通过多表查询的形式,将符合条件字段全部查出,筛选出权限字段。
SELECT m.perms
FROM`sys_user_role` as urLEFT JOIN `sys_role` as r on ur.role_id = r.idLEFT JOIN `sys_role_menu` as rm on ur.role_id = rm.role_idLEFT JOIN `sys_menu` as m on rm.menu_id = m.id
WHERE ur.`user_id` =2 AND r.`status`=0 and m.`status`=0;
我们之前UserDetail实现对象LoginUser的权限列表存静态权限,现在改为从数据库查询:
测试权限接口改为:
@RestController
public class TestController {@GetMapping("/hello")@PreAuthorize("hasAuthority('system:test:list')")public String hello(){return "hello";}
}
3.自定义失败处理
上面流程,使用postman进行测试的时候出现错误,springboot程序会直接错误响应如403、401;
但是我们希望程序正确响应,403等响应结果是由我们自定义公共对象封装而成。
WebUtil工具类
public class WebUtils {/*** 将字符串渲染到客户端* * @param response 渲染对象* @param string 待渲染的字符串* @return null*/public static String renderString(HttpServletResponse response, String string) {try{response.setStatus(200);response.setContentType("application/json");response.setCharacterEncoding("utf-8");response.getWriter().write(string);}catch (IOException e){e.printStackTrace();}return null;}}
AuthenticationEntryPoint实现
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {@Overridepublic void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {ResponseResult<Object> responseResult =new ResponseResult<>(401, "您认证错误/请检查你的用户名或密码是否正确");String jsonResult = JSON.toJSONString(responseResult);WebUtils.renderString(response,jsonResult);}
}
AccessDeniedHandler实现
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {@Overridepublic void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {ResponseResult<Object> responseResult =new ResponseResult<>(403, "权限不足");String jsonResult = JSON.toJSONString(responseResult);WebUtils.renderString(response,jsonResult);}
}
在SecurityConfig中加入下配置
4.跨域
前后端分离项目,都存在一个问题,跨域。
凡是,我们后端能就接受到前端原生response和request对象的都需要解决跨域问题。
SpringMvc跨域解决
@Configuration
public class CorsConfig implements WebMvcConfigurer {@Overridepublic void addCorsMappings(CorsRegistry registry) {// 设置允许跨域的路径registry.addMapping("/**")// 设置允许跨域请求的域名.allowedOriginPatterns("*")// 是否允许cookie.allowCredentials(true)// 设置允许的请求方式.allowedMethods("GET", "POST", "DELETE", "PUT")// 设置允许的header属性.allowedHeaders("*")// 跨域允许时间.maxAge(3600);}
}
SpringSecurity跨域解决
在security配置类加上如下配置。
原生设置
// 设置跨域response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin")); // 修改携带cookie,PSresponse.setHeader("Access-Control-Allow-Methods", "GET");response.setHeader("Access-Control-Allow-Headers", "Authorization,content-type"); // PS// 预检请求缓存时间(秒),即在这个时间内相同的预检请求不再发送,直接使用缓存结果。response.setHeader("Access-Control-Max-Age", "3600");