[Spring Security Series 1] Based on Spring Security to realize permission control of stateless Rest API by separating front and back ends

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

I. Introduction

In the past, when we used Spring Security to control the permissions of Webapp applications, it was a non-front-end and back-end separation architecture. We need to use the default login form provided by SpringSecurity, or use a custom login form to implement authentication. So, under the front-end and back-end separation architecture, how can we implement the use of Restful-style login interface for identity authentication, so as to realize the permission control of the entire application interface? Let's look at the implementation steps first, and then analyze the principle later.

2. Implementation steps

1. Introduce dependencies

<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. Rewrite the /login request of the Get method

Because of SpringSecurity's default Get method/login request, it will be redirected to the default login form, and the specific process will be analyzed later. This obviously does not meet the requirements of the front-end and back-end separation architecture, so we need to rewrite the specific code as follows:

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

The Result class is a generic response object defined, and the specific code can be found in the attached source code link.

3. Create AuthenticationRepository

In the actual production environment, we should store the authentication information in the cache or database. This is just a demonstration, and it is placed in memory. The specific code is as follows:

@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. Create authentication success handler TokenAuthenticationSuccessHandler and authentication failure handler TokenAuthenticationFailureHandler

SpringSecurity provides us with the authentication success interface AuthenticationSuccessHandler and the authentication failure handling interface AuthenticationFailureHandler. We only need to implement these two interfaces, and then implement the business logic we need. The specific code is as follows:

@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 is an encapsulated tool class that responds JSON data format to the front end through HttpServletResponse. For the specific code, please refer to the attached source code link.

5. Create exit success handler 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/…

复制代码

Guess you like

Origin juejin.im/post/7104466951379877925