【SpringSecurity系列1】基于SpringSecurity实现前后端分离无状态Rest API的权限控制

源码传送门:
https://github.com/ningzuoxin/zxning-springsecurity-demos/tree/master/01-springsecurity-stateless
复制代码

一、前言

以前我们在使用 SpringSecurity 来做 Webapp 应用的权限控制时,由于是非前后端分离架构。我们需要使用 SpringSecurity 提供的默认登录表单,或者使用自定义登录表单来实现身份认证。那么,在前后端分离架构下,我们该如何实现使用 Restful 风格的登录接口做身份认证,从而实现整个应用接口的权限控制呢?我们先看实现步骤,后续再来分析原理。

二、实现步骤

1、引入依赖

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.0</version>
</parent>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.8.1</version>
    </dependency>
</dependencies>
复制代码

2、改写 Get 方法的 /login 请求

由于 SpringSecurity 默认的 Get 方法 /login 请求,会重定向到默认的登录表单,具体流程我们后续分析。这显然是不满足前后端分离架构的需求,所以我们需要改写,具体代码如下:

@GetMapping(value = "/login")
public Result login() {
    return Result.data(-1, "PLEASE LOGIN", "NO LOGIN");
}
复制代码

Result 类是定义的一个通用响应对象,具体代码可查看附上的源码链接。

3、创建认证信息存储器 AuthenticationRepository

在实际生产环境中,我们应该把认证信息存储在缓存或者数据库中,此处只是演示,就放在内存中了。具体代码如下:

@Repository
public class AuthenticationRepository {

    private static ConcurrentHashMap<String, Authentication> authentications = new ConcurrentHashMap<>();

    public void add(String key, Authentication authentication) {
        authentications.put(key, authentication);
    }

    public Authentication get(String key) {
        return authentications.get(key);
    }

    public void delete(String key) {
        if (authentications.containsKey(key)) {
            authentications.remove(key);
        }
    }
}
复制代码

4、创建认证成功处理器 TokenAuthenticationSuccessHandler 和 认证失败处理器 TokenAuthenticationFailureHandler

SpringSecurity 为我们提供了认证成功接口 AuthenticationSuccessHandler 和 认证失败处理接口 AuthenticationFailureHandler,我们只需要实现这两个接口,然后实现我们需要的业务逻辑即可,具体代码如下:

@Component
public class TokenAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Autowired
    private AuthenticationRepository authenticationRepository;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        String token = IdUtil.simpleUUID();
        authenticationRepository.add(token, authentication);

        Result<String> result = Result.data(200, token, "LOGIN SUCCESS");
        HttpServletResponseUtils.print(response, result);
    }
}
复制代码
@Component
public class TokenAuthenticationFailureHandler implements AuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        Result<String> result = Result.data(-1, exception.getMessage(), "LOGIN FAILED");
        HttpServletResponseUtils.print(response, result);
    }
}
复制代码

HttpServletResponseUtils 是封装的一个通过 HttpServletResponse 向前端响应 JSON 数据格式的工具类,具体代码可查看附上的源码链接。

5、创建退出成功处理器 TokenLogoutSuccessHandler

@Component
public class TokenLogoutSuccessHandler implements LogoutSuccessHandler {

    @Autowired
    private AuthenticationRepository authenticationRepository;

    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        String token = request.getHeader("token");
        if (StrUtil.isNotEmpty(token)) {
            authenticationRepository.delete(token);
        }

        Result<String> result = Result.data(200, "LOGOUT SUCCESS", "OK");
        HttpServletResponseUtils.print(response, result);
    }
}
复制代码

6、创建无访问权限处理器 TokenAccessDeniedHandler

public class TokenAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        Result<String> result = Result.data(403, accessDeniedException.getMessage(), "ACCESS DENIED");
        HttpServletResponseUtils.print(response, result);
    }
}
复制代码

7、配置 WebSecurityConfig,这是重点!!!

创建 WebSecurityConfig 类,继承 WebSecurityConfigurerAdapter (最新的 SpringSecurity 版本,该类将被废弃,官方建议使用 @Bean 的配置方式),重写 configure(HttpSecurity http) 方法,整个SpringSecurity 的核心配置,都是基于 HttpSecurity 来实现的,具体配置如下:

@Bean
public UserDetailsService userDetailsService() {
    // 权限配置
    List<GrantedAuthority> authorities = new ArrayList<>();
    authorities.add(new SimpleGrantedAuthority("index"));
    authorities.add(new SimpleGrantedAuthority("hasAuthority"));
    authorities.add(new SimpleGrantedAuthority("ROLE_hasRole"));

    // 认证信息
    UserDetails userDetails = User.builder().username("admin").password(passwordEncoder().encode("123456")).authorities(authorities).build();
    return new InMemoryUserDetailsManager(userDetails);
}

@Override
protected void configure(HttpSecurity http) throws Exception {
    // 放行 /login 请求,其他请求必须经过认证
    http.authorizeRequests().antMatchers("/login").permitAll().anyRequest().authenticated()
            // 配置退出成功处理器
            .and().logout().logoutSuccessHandler(tokenLogoutSuccessHandler)
            // 配置无访问权限处理器
            .and().exceptionHandling().accessDeniedHandler(new TokenAccessDeniedHandler())
            // 指定登录请求url
            .and().formLogin().loginPage("/login")
            // 配置认证成功处理器
            .successHandler(tokenAuthenticationSuccessHandler)
            // 配置认证失败处理器
            .failureHandler(tokenAuthenticationFailureHandler)
            // 将 session 管理策略设置为 STATELESS (无状态)
            .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            // 禁用防止 csrf
            .and().csrf().disable()
            // 在 UsernamePasswordAuthenticationFilter 过滤器之前,添加自定义的 token 认证过滤器
            .addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
复制代码

8、创建一些测试用的 API 接口

@RestController
public class IndexController {

    @RequestMapping(value = "/index")
    @PreAuthorize("hasAuthority('index')")
    public String index() {
        return "index";
    }

    @RequestMapping(value = "/hasAuthority")
    @PreAuthorize("hasAuthority('hasAuthority')")
    public String hasAuthority() {
        return "hasAuthority";
    }

    @RequestMapping(value = "/hasRole")
    @PreAuthorize("hasRole('hasRole')")
    public String hasRole() {
        return "hasRole";
    }

    @RequestMapping(value = "/home")
    @PreAuthorize("hasRole('home')")
    public String home() {
        return "home";
    }

}
复制代码

三、测试

1、未登录访问受保护 API

// 请求地址 GET请求
http://localhost:8080/index

// curl
curl --location --request GET 'http://localhost:8080/index'

// 响应结果
{
    "code": -1,
    "msg": "NO LOGIN",
    "time": 1654131753192,
    "data": "PLEASE LOGIN"
}
复制代码

2、登录 API

// 请求地址 POST请求
http://localhost:8080/login?username=admin&password=123456

// curl
curl --location --request POST 'http://localhost:8080/login?username=admin&password=123456'

// 响应结果
{
    "code": 200,
    "msg": "LOGIN SUCCESS",
    "time": 1654131902130,
    "data": "612c29a2dd824191b6afe07a38285e81"
}
复制代码

3、携带 token 访问受保护 API

// 请求地址 GET请求 请求头中添加认证 token
http://localhost:8080/index

// curl
curl --location --request GET 'http://localhost:8080/index' --header 'token: 612c29a2dd824191b6afe07a38285e81'

// 响应结果
index
复制代码

4、携带 token 访问未授权 API

// 请求地址 GET请求 请求头中添加认证 token
http://localhost:8080/home

// curl
curl --location --request GET 'http://localhost:8080/home' --header 'token: 612c29a2dd824191b6afe07a38285e81'

// 响应结果
{
    "code": 403,
    "msg": "ACCESS DENIED",
    "time": 1654131989330,
    "data": "不允许访问"
}
复制代码

5、退出 API

// 请求地址 GET请求 请求头中添加认证 token
http://localhost:8080/logout

// curl
curl --location --request GET 'http://localhost:8080/logout' --header 'token: 612c29a2dd824191b6afe07a38285e81'

// 响应结果
{
    "code": 200,
    "msg": "OK",
    "time": 1654132037160,
    "data": "LOGOUT SUCCESS"
}
复制代码

四、总结

经过简单的改造之后,基本能满足前后端分离无状态 API 权限控制的需求。在应用于生产环境前,有两点需要进一步改造:

1、将身份认证和权限获取,改为从数据库中获取。

2、将通过认证的身份信息存储在缓存或数据库中。

在下一篇,我们将进一步分析实现原理,大家多多关注哦~

源码传送门:
https://github.com/ningzuoxin/zxning-springsecurity-demos/tree/master/01-springsecurity-stateless
复制代码

【打个广告】推荐下个人的基于 SpringCloud 开源项目,供大家学习参考,欢迎大家留言进群交流

Gitee:gitee.com/ningzxspace…

Github:github.com/ningzuoxin/…

复制代码

猜你喜欢

转载自juejin.im/post/7104466951379877925
今日推荐