SpringSecurity - 实战 RBAC(二)

前言

在上一篇文章 SpringSecurity - 实战 RBAC(一) 中,我们初步搭建了前后端项目,接下来这篇文章中我们就打通前后端项目,并实现初步的登录功能

概述

SpringSecurity 默认使用 Session 来保存用户登录状态,现在大部分都是前后端分离的项目,都会使用 token 来做身份认证,客户端在请求服务端的时候会在请求头中携带一个表示其身份的标识 token,一般都会使用 JWT,但基于我多年的 SpringSecurity 项目开发经验,JWT 中存储的信息一般不会太多,也不会把用户的资源授权信息存到 JWT 中,也就是说如果使用 JWT,那么到服务端的时候还是需要再去查询一次用户信息来获取到授权信息的;并且使用 JWT 还有一个缺点,就是 token 一旦下发给客户端,那么就不受服务端控制了,就不能做 被挤下线强制下线 等功能了,所以这里就不采用 JWT 这种普遍的解决方案

提供后端接口

1、引入 Redis

综上所述,我们引入 Redis 来存储 token,做第一步认证处理,如果 token 不存在则需要重新登录:

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-data-redisartifactId>
dependency>

并配置 application.yml

spring:
  redis:
    host: 192.168.228.18

2、关闭默认的登录

我们要做的是一个前后端分离的项目,所以并不需要默认的登录页面

@EnableWebSecurity
public class WebSecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                // 关闭默认的登录
                .formLogin().disable()
                .httpBasic().disable()
                // 关闭默认的登出
                .logout().disable()
                // 不使用 Session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        httpSecurity
                .authorizeRequests()
                // 所有请求都需要认证通过后才可以访问
                .anyRequest().authenticated();

        return httpSecurity.build();
    }
}

3、自定义登录接口 /login

因为接下来我们会对接数据库,所以使用 UserDetailsService 来模拟数据库操作,后面直接修改为查询数据库代码即可。同时 SpringSecurity 的配置也是比较有意思的,一环套一环,如果有不理解的地方,建议先把 SpringSecurity 学习 专栏的 启动流程分析 先阅读完,之后再来进行实战

  • 首先配置 WebSecurityConfig
@EnableWebSecurity
public class WebSecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                // 关闭 csrf 防御
                .csrf().disable()
                // 关闭默认的登录
                .formLogin().disable()
                .httpBasic().disable()
                // 关闭默认的登出
                .logout().disable()
                // 不使用 Session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        httpSecurity
                .authorizeRequests()
                // 所有请求都需要认证通过后才可以访问
                .anyRequest().authenticated();

        return httpSecurity.build();
    }

    /**
     * 这里不理解的可以查看 https://blog.csdn.net/qiaohao0206/article/details/126338492 这篇文章中对从 AuthenticationConfiguration 中获取 AuthenticationManager
     *
     * @param authenticationConfiguration
     * @return
     * @throws Exception
     */
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
  • 接下来提供登录接口:
// 登录接口请求参数
@Data
public class LoginParam {

    @NotBlank(message = "用户名不能为空")
    private String username;

    @NotBlank(message = "密码不能为空")
    private String password;
}
@RestController
public class LoginController {

    @Autowired
    private RedisUtils redisUtils;
    @Autowired
    private AuthenticationManager authenticationManager;

    @PostMapping("login")
    public R<?> login(@RequestBody @Valid LoginParam param) {
    	// 组装认证参数
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(param.getUsername(), param.getPassword());
        // 认证成功会返回 Authentication 对象
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
        // token
        String token= UUID.randomUUID().toString();
        // 把用户主体和 token 存到 redis 中
        redisUtils.set(token, authenticate.getPrincipal().toString(), 6, TimeUnit.HOURS);
        return R.success(token);
    }
}
  • 实现 UserDetailsService 接口,完善认证逻辑
// 加入到 IoC 容器中,会自动配置到认证逻辑中
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    	// 这里模拟查询数据库操作,后面对接数据库的时候只需要修改这里就可以了
        if ("admin".equals(username)) {
        	// 自定义的用户实体,实现 UserDetails 接口
            CurrentUser currentUser = new CurrentUser();
            currentUser.setUsername(username);
            currentUser.setPassword(passwordEncoder.encode("123456"));
            return currentUser;
        }
        throw new UsernameNotFoundException("账号或密码错误");
    }
}
  • 用户实体,这里也是简单配置一下,后面对接数据库的时候会完善用户信息:
@Data
public class CurrentUser implements UserDetails {

    private String username;

    private String password;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
    	// 提供一个默认的 sale 权限,后面对接数据库的时候再完善
        List<GrantedAuthority> authorityList = new ArrayList<>();
        SimpleGrantedAuthority grantedAuthority = new SimpleGrantedAuthority("sale");
        authorityList.add(grantedAuthority);
        return authorityList;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

    @Override
    public String toString() {
        return JSON.toJSONString(this);
    }
}

4、配置 /login 接口不需要认证

@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
    return web -> web.ignoring().antMatchers("/login");
}

5、使用 postman 请求 /login 接口

SpringSecurity - 实战 RBAC(二)_第1张图片

前端调用接口

1、修改请求地址

utils 包下的 request.js 中,我们可以看到 axios 使用 VUE_APP_BASE_API 作为请求地址,这里我们在 .env.development 中修改开发环境的请求地址:

# just a flag
ENV = 'development'

# base api
VUE_APP_BASE_API = 'http://127.0.0.1:9626'

2、修改请求接口

接着在 api 包下的 user.js 修改 login 方法中对于 /login 接口的请求:

export function login(data) {
  return request({
    url: '/login',
    method: 'post',
    data
  })
}

修改 store 包下的 modules 包中 user.js 中的 actions 中的 login 的返回结果处理:

login({ commit }, userInfo) {
  const { username, password } = userInfo
  return new Promise((resolve, reject) => {
    login({ username: username.trim(), password: password }).then(res => {
      const { data } = res
      commit('SET_TOKEN', data)
      setToken(data)
      resolve()
    }).catch(error => {
      reject(error)
    })
  })
}

views/login 包下的 index.vue 中,handleLogin 方法中我们看到登录请求成功之后,会跳转到 / 路径下,而在 permission.jsrouter 的前置拦截器中我们可以看到,当 Cookie 中已经存在 token,且请求的不是 /login 路径时,会判断 VueX 中是否包含 name 这一属性,如果没有的话会请求 user/getInfo 这一 action,通过查看代码,可以看到是请求的 api 目录下的 user.js 中的 getInfo 方法,所以还需要后端提供查询用户信息的接口,同时返回 nameavatar 两个参数:

@RestController
@RequestMapping("user")
public class UserController {

    @GetMapping("info")
    public R<Map<String, String>> userInfo(@AuthenticationPrincipal CurrentUser currentUser) {
        Map<String, String> result = new HashMap<>();
        result.put("name", currentUser.getUsername());
        result.put("avatar", "https://tupian.qqw21.com/article/UploadPic/2022-5/20225221202284170.jpg");
        return R.success(result);
    }
}

3、修改请求携带 token

  • 修改 utils 包下的 auth.jsCookie 中存放 token 的命名
import Cookies from 'js-cookie'

const TokenKey = 'token'

export function getToken() {
  return Cookies.get(TokenKey)
}

export function setToken(token) {
  return Cookies.set(TokenKey, token)
}

export function removeToken() {
  return Cookies.remove(TokenKey)
}

  • 修改 utils 包下的 reqeust.js 中请求携带 token 的命名

SpringSecurity - 实战 RBAC(二)_第2张图片

后端配置跨域

  • 配置 CorsFilterIoC 容器
@Configuration
public class CorsConfig {

    private CorsConfiguration buildConfig() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();

        corsConfiguration.addAllowedOriginPattern("*");

        corsConfiguration.addAllowedHeader("*");
        corsConfiguration.addAllowedMethod("*");
        corsConfiguration.setAllowCredentials(true);
        return corsConfiguration;
    }

    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", buildConfig());
        return new CorsFilter(source);
    }
}
  • 修改 WebSecurityConfig

SpringSecurity - 实战 RBAC(二)_第3张图片

总结

本篇文章完成了前后端的联调工作,需要注意跨域问题的解决和 CSRF 为什么要禁用,不懂的话可以看前几篇文章,接下来会对接数据库完成认证和前后端的鉴权

你可能感兴趣的:(#,SpringSecurity,学习,SpringSecurity,rbac)