1.认证
密码校验用户
密码加密存储
java
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
- 我们没有以上代码配置,默认明文存储, {id}password;
- 实现这个将passworxEncode变成@Bean之后,就变成加密存储: 加盐值 + 明文随机生成一段密文;
- 将返回UserDetails对象中的密码,与输入的密码进行校验
登录接口
- 在SpringSecurity放行登录接口,
- 定义AuthenticationManager @bean
java
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@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();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
PS:就我们研究框架, 有些值,需要我们利用断点调试,层层拨开来进行获取。
我们创建一个自定义登录接口,并把内部逻辑交给自定义UserService的实现类的方法来处理。
java
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private RedisCache redisCache;
@Override
public 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生成jwt
LoginUser 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);
// 把完整用户信息存入redis
redisCache.setCacheObject("login:"+userId,loginUser);
return new ResponseResult(200,"登录成功",map);
}
}
认证过滤器
java
@Component
public class JwtFilter extends OncePerRequestFilter {
@Autowired
private RedisCache redisCache;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//获取token
String token = request.getHeader("Authorization").substring(6);
if(StringUtils.isEmpty(token)){
filterChain.doFilter(request,response);
return;
}
//解析token
String 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过滤器链是默认放行的。
java
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtFilter jwtAuthenticationTokenFilter;
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@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();
//把token校验过滤器添加到过滤器链中,就是在 UsernamePasswordAuthenticationFilter之前
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
退出接口
- 对于每次请求都是不同的线程。
- 同一个运行程序,一般的静态变量可以被不同线程访问;
- ThreadLocal的静态变量,是线程隔离,只有当前线程才能访问;
- 因此,每次访问的SecurityContextHolder默认是空的,只有经过jwt过滤器之后,变得有意义;
2.授权
我们前面是基于认证的流程,但是实际上,我们认证之后,不同等级用户,需要不同的权限访问。
- 从SpringSecurityInterceptor进行权限校验,那么我们要配置让他权限校验。
限制接口访问权限
springSecurity开启权限配置,1.注解形式 2.配置文件(java配置类)形式
封装权限信息
UserDetailService修改:
UserDetails修改:
java
@Data
@NoArgsConstructor
public class LoginUser implements UserDetails {
private User user;
private List<String> authList;
@JSONField(serialize = false)
private List<SimpleGrantedAuthority> authorities; // SimpleGrantedAuthority对象不支持序列化,无法存入redis
public LoginUser(User user, List<String> authList) { // 将对应的权限字符串列表传入
this.user = user;
this.authList = authList;
}
@Override
public 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;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUserName();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
认证过滤器更改:将权限信息增加到contex中,以便后续拦截器链获取。
从数据库查询信息
RBAC权限模型
RBAC权限模型(Role-Based Access Control)即:基于角色的权限控制。这是目前最常被开发者使用也是相对易用、通用权限模型。
涉及到的sql语句
通过多表查询的形式,将符合条件字段全部查出,筛选出权限字段。
sql
SELECT
m.perms
FROM
`sys_user_role` as ur
LEFT JOIN `sys_role` as r on ur.role_id = r.id
LEFT JOIN `sys_role_menu` as rm on ur.role_id = rm.role_id
LEFT 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的权限列表存静态权限,现在改为从数据库查询:
测试权限接口改为:
java
@RestController
public class TestController {
@GetMapping("/hello")
@PreAuthorize("hasAuthority('system:test:list')")
public String hello(){
return "hello";
}
}
3.自定义失败处理
上面流程,使用postman进行测试的时候出现错误,springboot程序会直接错误响应如403、401;
但是我们希望程序正确响应,403等响应结果是由我们自定义公共对象封装而成。
WebUtil工具类
java
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实现
java
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public 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实现
java
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
@Override
public 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跨域解决
java
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
// 设置允许跨域的路径
registry.addMapping("/**")
// 设置允许跨域请求的域名
.allowedOriginPatterns("*")
// 是否允许cookie
.allowCredentials(true)
// 设置允许的请求方式
.allowedMethods("GET", "POST", "DELETE", "PUT")
// 设置允许的header属性
.allowedHeaders("*")
// 跨域允许时间
.maxAge(3600);
}
}
SpringSecurity跨域解决
在security配置类加上如下配置。
原生设置
java
// 设置跨域
response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin")); // 修改携带cookie,PS
response.setHeader("Access-Control-Allow-Methods", "GET");
response.setHeader("Access-Control-Allow-Headers", "Authorization,content-type"); // PS
// 预检请求缓存时间(秒),即在这个时间内相同的预检请求不再发送,直接使用缓存结果。
response.setHeader("Access-Control-Max-Age", "3600");