源码传送门:
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:https://gitee.com/ningzxspace/exam-ning-springcloud-v1

Github:https://github.com/ningzuoxin/exam-ning-springcloud-v1

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

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

    源码传送门: https://github.com/ningzuoxin/zxning-springsecurity-demos/tree/master/02-springsecurity-state ...

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

    源码传送门: https://github.com/ningzuoxin/zxning-springsecurity-demos/tree/master/01-springsecurity-state ...

  3. 用Spring Security, JWT, Vue实现一个前后端分离无状态认证Demo

    简介 完整代码 https://github.com/PuZhiweizuishuai/SpringSecurity-JWT-Vue-Deom 运行展示 后端 主要展示 Spring Security ...

  4. 基于 koajs 的前后端分离实践

    一.什么是前后端分离? 前后端分离的概念和优势在这里不再赘述,有兴趣的同学可以看各个前辈们一系列总结和讨论: 系列文章:前后端分离的思考与实践(1-6) slider: 淘宝前后端分离实践 知乎提问: ...

  5. (转)也谈基于NodeJS的全栈式开发(基于NodeJS的前后端分离)

    原文链接:http://ued.taobao.org/blog/2014/04/full-stack-development-with-nodejs/ 随着不同终端(pad/mobile/pc)的兴起 ...

  6. 也谈基于NodeJS的全栈式开发(基于NodeJS的前后端分离)

    前言 为了解决传统Web开发模式带来的各种问题,我们进行了许多尝试,但由于前/后端的物理鸿沟,尝试的方案都大同小异.痛定思痛,今天我们重新思考了“前后端”的定义,引入前端同学都熟悉的NodeJS,试图 ...

  7. 基于NodeJS进行前后端分离

    1.什么是前后端分离 传统的SPA模式:所有用到的展现数据都是后端通过异步接口(AJAX/JSONP)的方式提供的,前端只管展现. 从某种意义上来说,SPA确实做到了前后端分离,但这种方式存在两个问题 ...

  8. [原创]基于VueJs的前后端分离框架搭建之完全攻略

    首先请原谅本文标题取的有点大,但并非为了哗众取宠.本文取这个标题主要有3个原因,这也是写作本文的初衷: (1)目前国内几乎搜索不到全面讲解如何搭建前后端分离框架的文章,讲前后端分离框架思想的就更少了, ...

  9. [转] 基于NodeJS的前后端分离的思考与实践(五)多终端适配

    前言 近年来各站点基于 Web 的多终端适配进行得如火如荼,行业间也发展出依赖各种技术的解决方案.有如基于浏览器原生 CSS3 Media Query 的响应式设计.基于云端智能重排的「云适配」方案等 ...

随机推荐

  1. clone github代码库很慢,如何提速

    博主,最近在搭建hyperledger fabric的环境,其中有一步就是clone github上的代码,但是在过程中发现clone是真的慢. 为此google了一圈,发一现一个好用的办法: 1.登 ...

  2. Spring配置数据源(连接池)

    1.数据源(连接池)的作用:为了提高程序的性能而出现的 2.数据源的原理: *事先实例化数据源,初始化部分连接资源 *使用连接资源时从数据源中获取 *使用完毕后将连接资源归还给数据源 使用c3p0的步 ...

  3. 帝国cms7.5忘记登录密码以及多次登录失败被锁定终极解决办法

    其实网上很多忘记登录密码的解决方法都是相对于7.5版本以下的,在7.5的版本里根本不适用,今天小编主要给大家说的是针对帝国cms7.5忘记登录密码正确的解决办法. 前提是你能登陆服务器: 一.忘记登录 ...

  4. 使用 Nginx 实现 URL 的重定向

    1. 概述 老话说的好:取乎上,得其中:取乎中,得其下.因此我们不妨把目标定的高一些,去努力,才能得到更好回报. 言归正传,今天我们来聊聊 使用 Nginx 实现 URL 的重定向. 2. 使用 Ng ...

  5. Python打印表格

    使用Python在终端打印表格 import prettytable table = PrettyTable(['Title1', 'Title2', 'Title3']) table.add_row ...

  6. OSPF MTU问题

    OSFP(开放式最短路径优先)链路状态协议,IGP 1.mtu 检测 链路俩段不匹配 假设双方的mtu不一致时 ospf建立如下: R1与R2交互hello报文,其中包含:目的IP地址:224.0.0 ...

  7. [cf]Codeforces Round #784(Div 4)

    由于一次比赛被虐得太惨,,生发开始写blog的想法,于是便有了这篇随笔(找了个近期的cf比赛练练手(bushi))第一次写blog,多多包涵. 第二场cf比赛,第一场打的Div2,被虐太惨,所以第二场 ...

  8. python数据可视化-matplotlib入门(6)-从文件中加载数据

    前几篇都是手动录入或随机函数产生的数据.实际有许多类型的文件,以及许多方法,用它们从文件中提取数据来图形化. 比如之前python基础(12)介绍打开文件的方式,可直接读取文件中的数据,扩大了我们的数 ...

  9. Linux-流程控制语法-函数-属组

    1.流程控制 1.1 条件选择 1.1.1选择执行if语句 格式: if COMMANDS; then COMMANDS; [ elif COMMANDS; then COMMANDS; ] ... ...

  10. [笔记] 2-sat

    定义 简单的说就是给出 \(n\) 个集合,每个集合有两个元素,已知形如选 \(a\) 则必须选 \(b\) 的若干个条件, 问是否存在从每个集合选择一个元素满足条件的方案,通常可以题目只要求任意一种 ...