小二是新来的实习生,作为技术 leader,我还是很负责任的,有什么锅都想甩给他,啊,不,一不小心怎么把心里话全说出来了呢?重来!

小二是新来的实习生,作为技术 leader,我还是很负责任的,有什么好事都想着他,这不,我就安排了一个整合SpringSecurity+JWT实现登录认证的小任务交,没想到,他仅用四步就搞定了,这让我感觉倍有面。

一、关于 SpringSecurity

在 Spring Boot 出现之前,SpringSecurity 的使用场景是被另外一个安全管理框架 Shiro 牢牢霸占的,因为相对于 SpringSecurity 来说,SSM 中整合 Shiro 更加轻量级。Spring Boot 出现后,使这一情况情况大有改观。正应了那句古话:一人得道鸡犬升天,虽然有点不大合适,就将就着用吧。

这是因为 Spring Boot 为 SpringSecurity 提供了自动化配置,大大降低了 SpringSecurity 的学习成本。另外,SpringSecurity 的功能也比 Shiro 更加强大。

二、关于 JWT

JWT,是目前最流行的一个跨域认证解决方案:客户端发起用户登录请求,服务器端接收并认证成功后,生成一个 JSON 对象(如下所示),然后将其返回给客户端。

从本质上来说,JWT 就像是一种生成加密用户身份信息的 Token,更安全也更灵活。

三、整合步骤

第一步,给需要登录认证的模块添加 codingmore-security 依赖:

<dependency>
<groupId>top.codingmore</groupId>
<artifactId>codingmore-security</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>

比如说 codingmore-admin 后端管理模块需要登录认证,就在 codingmore-admin/pom.xml 文件中添加 codingmore-security 依赖。

第二步,在需要登录认证的模块里添加 CodingmoreSecurityConfig 类,继承自 codingmore-security 模块中的 SecurityConfig 类。

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class CodingmoreSecurityConfig extends SecurityConfig {
@Autowired
private IUsersService usersService; @Bean
public UserDetailsService userDetailsService() {
//获取登录用户信息
return username -> usersService.loadUserByUsername(username);
}
}

UserDetailsService 这个类主要是用来加载用户信息的,包括用户名、密码、权限、角色集合....其中有一个方法如下:

UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

认证逻辑中,SpringSecurity 会调用这个方法根据客户端传入的用户名加载该用户的详细信息,包括判断:

  • 密码是否一致
  • 通过后获取权限和角色
    public UserDetails loadUserByUsername(String username) {
// 根据用户名查询用户
Users admin = getAdminByUsername(username);
if (admin != null) {
List<Resource> resourceList = getResourceList(admin.getId());
return new AdminUserDetails(admin,resourceList);
}
throw new UsernameNotFoundException("用户名或密码错误");
}

getAdminByUsername 负责根据用户名从数据库中查询出密码、角色、权限等。

    public Users getAdminByUsername(String username) {
QueryWrapper<Users> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("user_login", username);
List<Users> usersList = baseMapper.selectList(queryWrapper); if (usersList != null && usersList.size() > 0) {
return usersList.get(0);
} // 用户名错误,提前抛出异常
throw new UsernameNotFoundException("用户名错误");
}

第三步,在 application.yml 中配置下不需要安全保护的资源路径:

secure:
ignored:
urls: #安全路径白名单
- /doc.html
- /swagger-ui/**
- /swagger/**
- /swagger-resources/**
- /**/v3/api-docs
- /**/*.js
- /**/*.css
- /**/*.png
- /**/*.ico
- /webjars/springfox-swagger-ui/**
- /actuator/**
- /druid/**
- /users/login
- /users/register
- /users/info
- /users/logout

第四步,在登录接口中添加登录和刷新 token 的方法:

@Controller
@Api(tags = "用户")
@RequestMapping("/users")
public class UsersController {
@Autowired
private IUsersService usersService;
@Value("${jwt.tokenHeader}")
private String tokenHeader;
@Value("${jwt.tokenHead}")
private String tokenHead; @ApiOperation(value = "登录以后返回token")
@RequestMapping(value = "/login", method = RequestMethod.POST)
@ResponseBody
public ResultObject login(@Validated UsersLoginParam users, BindingResult result) {
String token = usersService.login(users.getUserLogin(), users.getUserPass()); if (token == null) {
return ResultObject.validateFailed("用户名或密码错误");
} // 将 JWT 传递回客户端
Map<String, String> tokenMap = new HashMap<>();
tokenMap.put("token", token);
tokenMap.put("tokenHead", tokenHead);
return ResultObject.success(tokenMap);
} @ApiOperation(value = "刷新token")
@RequestMapping(value = "/refreshToken", method = RequestMethod.GET)
@ResponseBody
public ResultObject refreshToken(HttpServletRequest request) {
String token = request.getHeader(tokenHeader);
String refreshToken = usersService.refreshToken(token);
if (refreshToken == null) {
return ResultObject.failed("token已经过期!");
}
Map<String, String> tokenMap = new HashMap<>();
tokenMap.put("token", refreshToken);
tokenMap.put("tokenHead", tokenHead);
return ResultObject.success(tokenMap);
}
}

使用 Apipost 来测试一下,首先是文章获取接口,在没有登录的情况下会提示暂未登录或者 token 已过期。

四、实现原理

小二之所以能仅用四步就实现了登录认证,主要是因为他将 SpringSecurity+JWT 的代码封装成了通用模块,我们来看看 codingmore-security 的目录结构。

codingmore-security
├── component
| ├── JwtAuthenticationTokenFilter -- JWT登录授权过滤器
| ├── RestAuthenticationEntryPoint
| └── RestfulAccessDeniedHandler
├── config
| ├── IgnoreUrlsConfig
| └── SecurityConfig
└── util
└── JwtTokenUtil -- JWT的token处理工具类

JwtAuthenticationTokenFilter 和 JwtTokenUtil 在讲 JWT 的时候已经详细地讲过了,这里再简单补充一点。

客户端的请求头里携带了 token,服务端肯定是需要针对每次请求解析校验 token 的,所以必须得定义一个过滤器,也就是 JwtAuthenticationTokenFilter:

  • 从请求头中获取 token
  • 对 token 进行解析、验签、校验过期时间
  • 校验成功,将验证结果放到 ThreadLocal 中,供下次请求使用

重点来看其他四个类。第一个 RestAuthenticationEntryPoint(自定义返回结果:未登录或登录过期):

public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Cache-Control","no-cache");
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
response.getWriter().println(JSONUtil.parse(ResultObject.unauthorized(authException.getMessage())));
response.getWriter().flush();
}
}

可以通过 debug 的方式看一下返回的信息正是之前用户未登录状态下访问文章页的错误信息。

具体的信息是在 ResultCode 类中定义的。

public enum ResultCode implements IErrorCode {
SUCCESS(0, "操作成功"),
FAILED(500, "操作失败"),
VALIDATE_FAILED(506, "参数检验失败"),
UNAUTHORIZED(401, "暂未登录或token已经过期"),
FORBIDDEN(403, "没有相关权限");
private long code;
private String message; private ResultCode(long code, String message) {
this.code = code;
this.message = message;
}
}

第二个 RestfulAccessDeniedHandler(自定义返回结果:没有权限访问时):

public class RestfulAccessDeniedHandler implements AccessDeniedHandler{
@Override
public void handle(HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException e) throws IOException, ServletException {
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Cache-Control","no-cache");
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
response.getWriter().println(JSONUtil.parse(ResultObject.forbidden(e.getMessage())));
response.getWriter().flush();
}
}

第三个IgnoreUrlsConfig(用于配置不需要安全保护的资源路径):

@Getter
@Setter
@ConfigurationProperties(prefix = "secure.ignored")
public class IgnoreUrlsConfig {
private List<String> urls = new ArrayList<>();
}

通过 lombok 注解的方式直接将配置文件中不需要权限校验的路径放开,比如说 Knife4j 的接口文档页面。如果不放开的话,就被 SpringSecurity 拦截了,没办法访问到了。

第四个SecurityConfig(SpringSecurity通用配置):

public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired(required = false)
private DynamicSecurityService dynamicSecurityService; @Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity
.authorizeRequests(); //不需要保护的资源路径允许访问
for (String url : ignoreUrlsConfig().getUrls()) {
registry.antMatchers(url).permitAll();
} // 任何请求需要身份认证
registry.and()
.authorizeRequests()
.anyRequest()
.authenticated()
// 关闭跨站请求防护及不使用session
.and()
.csrf()
.disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// 自定义权限拒绝处理类
.and()
.exceptionHandling()
.accessDeniedHandler(restfulAccessDeniedHandler())
.authenticationEntryPoint(restAuthenticationEntryPoint())
// 自定义权限拦截器JWT过滤器
.and()
.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
//有动态权限配置时添加动态权限校验过滤器
if(dynamicSecurityService!=null){
registry.and().addFilterBefore(dynamicSecurityFilter(), FilterSecurityInterceptor.class);
}
}
}

这个类的主要作用就是告诉 SpringSecurity 那些路径不需要拦截,除此之外的,都要进行 RestfulAccessDeniedHandler(登录校验)、RestAuthenticationEntryPoint(权限校验)和 JwtAuthenticationTokenFilter(JWT 过滤)。

并且将 JwtAuthenticationTokenFilter 过滤器添加到 UsernamePasswordAuthenticationFilter 过滤器之前。

五、测试

第一步,测试登录接口,Apipost 直接访问 http://localhost:9002/users/login,可以看到 token 正常返回。

第二步,不带 token 直接访问文章接口,可以看到进入了 RestAuthenticationEntryPoint 这个处理器:

第三步,携带 token,这次我们改用 Knife4j 来测试,发现可以正常访问:

源码链接:

https://github.com/itwanger/coding-more

本篇已收录至 GitHub 上星标 1.9k+ star 的开源专栏《Java 程序员进阶之路》,据说每一个优秀的 Java 程序员都喜欢她,风趣幽默、通俗易懂。内容包括 Java 基础、Java 并发编程、Java 虚拟机、Java 企业级开发、Java 面试等核心知识点。学 Java,就认准 Java 程序员进阶之路。

https://github.com/itwanger/toBeBetterJavaer

star 了这个仓库就等于你拥有了成为了一名优秀 Java 工程师的潜力。也可以戳下面的链接跳转到《Java 程序员进阶之路》的官网网址,开始愉快的学习之旅吧。

https://tobebetterjavaer.com/

没有什么使我停留——除了目的,纵然岸旁有玫瑰、有绿荫、有宁静的港湾,我是不系之舟。

厉害!我带的实习生仅用四步就整合好SpringSecurity+JWT实现登录认证!的更多相关文章

  1. iOS App转让、转移、迁移(App transfer) -- 仅需四步

    当需要将某个 App 出售给其他开发人员,或想要将其移至其他 App Store Connect 组织,则您需要转让该 App.您无需将 App 从 App Store 下架,即可将其所有权转让给另一 ...

  2. 【项目实践】一文带你搞定Session和JWT的登录认证方式

    以项目驱动学习,以实践检验真知 前言 登录认证,估计是所有系统中最常见的功能了,并且也是最基础.最重要的功能.为了做好这一块而诞生了许多安全框架,比如最常见的Shiro.Spring Security ...

  3. 在深谈TCP/IP三步握手&四步挥手原理及衍生问题—长文解剖IP

    如果对网络工程基础不牢,建议通读<细说OSI七层协议模型及OSI参考模型中的数据封装过程?> 下面就是TCP/IP(Transmission Control Protoco/Interne ...

  4. 只需十四步:从零开始掌握 Python 机器学习(附资源)

    分享一篇来自机器之心的文章.关于机器学习的起步,讲的还是很清楚的.原文链接在:只需十四步:从零开始掌握Python机器学习(附资源) Python 可以说是现在最流行的机器学习语言,而且你也能在网上找 ...

  5. 只需十四步:从零开始掌握Python机器学习(附资源)

    转载:只需十四步:从零开始掌握Python机器学习(附资源) Python 可以说是现在最流行的机器学习语言,而且你也能在网上找到大量的资源.你现在也在考虑从 Python 入门机器学习吗?本教程或许 ...

  6. 用PHPcms V9四步完成WAP手机站搭建

    用PHPCMS最新发布的V9搭建了ONOW中文网,WEB网站(www.onow.cn)完成后,有用户提供手机访问的问题, 于是着手搭建ONOW手机WAP站(3g.onow.cn). 用PHPCMS V ...

  7. 组件 layui 表单抓取数据四步走

    注意事项: layui 中提交按钮是基于"监听"机制实现的. form.on() 的调用需置于 layui.use 的回调函数中. 末尾的 'return false' 不可或缺, ...

  8. 关于pythoh面向过程开发人员三步转面向对象的补充,再加一步,四步走战略。转面向对象也可以有固定公式。

    前言: oop非常非常非常重要.搞不懂oop,就玩不了python,就算能写也一定是写代码时候喜欢靠猜瞎猫碰死老鼠写得心很虚.为什么这么说呢,我也是从面向过程编程到死走过来的,一路def到死,一看到有 ...

  9. {Django基础九之中间件} 一 前戏 二 中间件介绍 三 自定义中间件 四 中间件的执行流程 五 中间件版登陆认证

    Django基础九之中间件 本节目录 一 前戏 二 中间件介绍 三 自定义中间件 四 中间件的执行流程 五 中间件版登陆认证 六 xxx 七 xxx 八 xxx 一 前戏 我们在前面的课程中已经学会了 ...

随机推荐

  1. HCNP Routing&Switching之组播技术-组播分发

    前文我们了解了组播技术中的igmp-snooping相关话题,回顾请参考https://www.cnblogs.com/qiuhom-1874/p/15860484.html:今天我们来聊一聊组播技术 ...

  2. MySQL8.0降级安装5.7

    本文旨在自我学习使用,如有任何疑问请及时联系博主 前言 基于OpenHarmony的FA数字管家服务端 默认情况下,Ubuntu20.04安装MySQL的版本为8.0.但8.0更加严格的加密规则,使得 ...

  3. k8s集群关机后,如何解决 kubernetes 重启起不来的问题

    如何解决 kubernetes 重启后,启来不来的问题 登录自己的Kubernetes测试集群时发现集群好像没有启动成功 运行 kubectl get pods --all -A ,报错如下. 第一反 ...

  4. 关于使用charles进行mock步骤

    1.应用场景: 1.1 测试环境中,可能存在部分依赖外部系统的http接口,因为外部系统未部署或服务不可用等其他原因,导致接口无法正常响应(特别是qa1环境): 而平时的测试主要还是以界面上的功能测试 ...

  5. 哪一个List实现了最快插入?

    LinkedList和ArrayList是另个不同变量列表的实现.ArrayList的优势在于动态的增长数组,非常适合初始时总长度未知的情况下使用.LinkedList的优势在于在中间位置插入和删除操 ...

  6. 客户端注册 Watcher 实现 ?

    1.调用 getData()/getChildren()/exist()三个 API,传入 Watcher 对象 2.标记请求 request,封装 Watcher 到 WatchRegistrati ...

  7. Python - time标准库使用与程序计时

  8. C语言思维导图—自己整理的

  9. 学习webpack前的准备工作

    前言 由于vue和react的流行,webpack这个模块化打包工具也已经成为热门.作为前端工程师这个需要不断更新自己技术库的职业,真的需要潜下心来学习一下. 准备工作(针对mac用户) 安装 hom ...

  10. 浏览器默认解析文档模式IE7

    背景:实习期间开发了一套页面,兼容到IE8+,可是在给产品体验时,他的QQ浏览器默认解析到了文档模式IE7,出现了页面的兼容性问题.问题排查:QQ浏览器目前有两个模式,极速模式和兼容模式,其中极速模式 ...