Spring Security整合JWT,实现单点登录,So Easy~!
前面整理过一篇 SpringBoot Security前后端分离,登录退出等返回json数据,也就是用Spring Security,基于SpringBoot2.1.4 RELEASE前后端分离的情况下,实现了登陆登出的功能,亮点就在于以JSON的形式接收返回参数。这个是针对单个后台服务的, 登录信息都存储在SecurityContextHolder缓存里。如果是两个或两个以上的应用呢,那该怎么办?Session是不能用了,Cookie自然也不能用,毕竟它俩是一对的。
曾想过用OAuth2来解决这个问题,但是OAuth2太复杂,首先理解概念就需要花费一些时间,而且里面的授权服务器、资源服务器、客户端等等让人傻傻分不清,还有四种授权模式,要反复衡量,到底要用哪一种,概念还没有扯清楚就开始纠结使用哪一个了。从概念入手不是个好主意,也不是个轻松的主意。在理解OAuth2的过程中,想到自己的项目是前后端分离的,离不开JSON,无意中遇见JWT。JWT是什么玩意,咦,难道是自己苦苦寻求的吗?!
那么,什么是JWT呢?看看专家介绍 阮一峰的网络日志,才知道,JWT 是JSON Web Token的简称,它解决的就是跨域问题。看来,要找的就是它,简单的,但也是管用的。
继续深究,JWT到底是怎样和SpringSecurity结合的呢。下面上代码,在上代码前先说明一下,在本次实例中,涉及到两个项目,一个项目是登录的项目A,另一个项目是根据token进行访问的项目B。其中B项目没有登录,也不会涉及登录,只要有Token就可以访问,Token失效了就访问不了了。
A项目是登录的项目,也是一个只能通过登录进行访问的后台服务。B项目就是一个服务,只要用户在A项目登录了,就可以访问。

A项目配置,代码如下
第一步,A项目 POM.xml 引入文件
<!-- spring-security 和 jwt 引入 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
第二步,A项目SecurityConfig配置
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import com.example.demo.filter.JWTAuthenticationFilter;
import com.example.demo.filter.JWTLoginFilter;
/**
* SpringSecurity的配置
* 参考网址:https://blog.csdn.net/sxdtzhaoxinguo/article/details/77965226
* @author 程就人生
* @date 2019年5月26日
*/
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService myCustomUserService;
@Autowired
private MyPasswordEncoder myPasswordEncoder;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//关闭跨站请求防护
.cors().and().csrf().disable()
//允许不登陆就可以访问的方法,多个用逗号分隔
.authorizeRequests().antMatchers("/test").permitAll()
//其他的需要授权后访问
.anyRequest().authenticated()
.and()
//增加登录拦截
.addFilter(new JWTLoginFilter(authenticationManager()))
//增加是否登陸过滤
.addFilter(new JWTAuthenticationFilter(authenticationManager()))
// 前后端分离是无状态的,所以暫時不用session,將登陆信息保存在token中。
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
//覆盖UserDetailsService类
auth.userDetailsService(myCustomUserService)
//覆盖默认的密码验证类
.passwordEncoder(myPasswordEncoder);
}
}
第三步,实现配置文件中自定义的类
- MyPasswordEncoder类实现了默认的PasswordEncoder 接口,可以对密码加密和密码对比进行个性化定制
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
/**
* 自定义的密码加密方法,实现了PasswordEncoder接口
* @author 程就人生
* @date 2019年5月26日
*/
@Component
public class MyPasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence charSequence) {
//加密方法可以根据自己的需要修改
return charSequence.toString();
}
@Override
public boolean matches(CharSequence charSequence, String s) {
return encode(charSequence).equals(s);
}
}
- MyCustomUserService 实现了框架默认的UserDetailsService,可以根据username从数据库获取用户,查看用户是否存在
/**
* 登录专用类,用户登陆时,通过这里查询数据库
* 自定义类,实现了UserDetailsService接口,用户登录时调用的第一类
* @author 程就人生
* @date 2019年5月26日
*/
@Component
public class MyCustomUserService implements UserDetailsService {
/**
* 登陆验证时,通过username获取用户的所有权限信息
* 并返回UserDetails放到spring的全局缓存SecurityContextHolder中,以供授权器使用
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//在这里可以自己调用数据库,对username进行查询,看看在数据库中是否存在
MyUserDetails myUserDetail = new MyUserDetails();
myUserDetail.setUsername(username);
myUserDetail.setPassword("123456");
return myUserDetail;
}
}
- MyUserDetails 实现了框架的UserDetails接口,可以在该类中根据需要添加自己必需的属性
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
/**
* 实现了UserDetails接口,只留必需的属性,也可添加自己需要的属性
* @author 程就人生
* @date 2019年5月26日
*/
public class MyUserDetails implements UserDetails {
private static final long serialVersionUID = 1L;
//登录用户名
private String username;
//登录密码
private String password;
private Collection<? extends GrantedAuthority> authorities;
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
public void setAuthorities(Collection<? extends GrantedAuthority> authorities) {
this.authorities = authorities;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.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;
}
}
- JWTLoginFilter 实现了框架自带的UsernamePasswordAuthenticationFilter 接口,对拦截做处理,以便登录成功后,在头部设置token返回;不管登录成功还是失败,都有JSON数据返回
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import com.example.demo.entity.User;
import com.example.demo.security.MyUserDetails;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
/**
* 验证用户名密码正确后,生成一个token,放在header里,返回给客户端
* @author 程就人生
* @date 2019年5月26日
*/
public class JWTLoginFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
public JWTLoginFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
/**
* 接收并解析用户凭证,出現错误时,返回json数据前端
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res){
try {
User user =new ObjectMapper().readValue(req.getInputStream(), User.class);
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
user.getUsername(),
user.getPassword(),
new ArrayList<>())
);
} catch (Exception e) {
try {
//未登錄出現賬號或密碼錯誤時,使用json進行提示
res.setContentType("application/json;charset=utf-8");
res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
PrintWriter out = res.getWriter();
Map<String,Object> map = new HashMap<String,Object>();
map.put("code",HttpServletResponse.SC_UNAUTHORIZED);
map.put("message","账号或密码错误!");
out.write(new ObjectMapper().writeValueAsString(map));
out.flush();
out.close();
} catch (Exception e1) {
e1.printStackTrace();
}
throw new RuntimeException(e);
}
}
/**
* 用户登录成功后,生成token,并且返回json数据给前端
*/
@Override
protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res,FilterChain chain, Authentication auth){
//json web token构建
String token = Jwts.builder()
//此处为自定义的、实现org.springframework.security.core.userdetails.UserDetails的类,需要和配置中设置的保持一致
//此处的subject可以用一个用户名,也可以是多个信息的组合,根据需要来定
.setSubject(((MyUserDetails) auth.getPrincipal()).getUsername())
//设置token过期时间,24小時
.setExpiration(new Date(System.currentTimeMillis() + 60 * 60 * 24 * 1000))
//设置token签名、密钥
.signWith(SignatureAlgorithm.HS512, "MyJwtSecret")
.compact();
//返回token
res.addHeader("Authorization", "Bearer " + token);
try {
//登录成功時,返回json格式进行提示
res.setContentType("application/json;charset=utf-8");
res.setStatus(HttpServletResponse.SC_OK);
PrintWriter out = res.getWriter();
Map<String,Object> map = new HashMap<String,Object>();
map.put("code",HttpServletResponse.SC_OK);
map.put("message","登陆成功!");
out.write(new ObjectMapper().writeValueAsString(map));
out.flush();
out.close();
} catch (Exception e1) {
e1.printStackTrace();
}
}
}
- JWTAuthenticationFilter 类实现了BasicAuthenticationFilter 接口,对Controller中需要登录后才能访问的方法进行了拦截,没有登录,则不能访问,返回JSON信息进行提示
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.Jwts;
/**
* 是否登陆验证方法
* @author 程就人生
* @date 2019年5月26日
*/
public class JWTAuthenticationFilter extends BasicAuthenticationFilter {
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
/**
* 對請求進行過濾
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) {
try {
//请求体的头中是否包含Authorization
String header = request.getHeader("Authorization");
//Authorization中是否包含Bearer,有一个不包含时直接返回
if (header == null || !header.startsWith("Bearer ")) {
chain.doFilter(request, response);
responseJson(response);
return;
}
//获取权限失败,会抛出异常
UsernamePasswordAuthenticationToken authentication = getAuthentication(request);
//获取后,将Authentication写入SecurityContextHolder中供后序使用
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
} catch (Exception e) {
responseJson(response);
e.printStackTrace();
}
}
/**
* 未登錄時的提示
* @param response
*/
private void responseJson(HttpServletResponse response){
try {
//未登錄時,使用json進行提示
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
PrintWriter out = response.getWriter();
Map<String,Object> map = new HashMap<String,Object>();
map.put("code",HttpServletResponse.SC_FORBIDDEN);
map.put("message","请登录!");
out.write(new ObjectMapper().writeValueAsString(map));
out.flush();
out.close();
} catch (Exception e1) {
e1.printStackTrace();
}
}
/**
* 通过token,获取用户信息
* @param request
* @return
*/
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
String token = request.getHeader("Authorization");
if (token != null) {
//通过token解析出用户信息
String user = Jwts.parser()
//签名、密钥
.setSigningKey("MyJwtSecret")
.parseClaimsJws(token.replace("Bearer ", ""))
.getBody()
.getSubject();
//不为null,返回
if (user != null) {
return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>());
}
return null;
}
return null;
}
}
- 在登录过滤器中接收参数的实体类,也可以直接接收,这一个类不是必须的
public class User {
private long id;
private String username;
private String password;
public long getId() {
return id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
B项目的配置
第一步,在pom中引入必须的架包
<!-- spring-security 和 jwt 引入 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
第二步,增加SecurityConfig配置文件
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import com.example.demo.filter.JWTAuthenticationFilter;
/**
* SpringSecurity的配置
* 参考网址:https://blog.csdn.net/sxdtzhaoxinguo/article/details/77965226
* @author 程就人生
* @date 2019年5月26日
*/
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//关闭跨站请求防护
.cors().and().csrf().disable()
//允许不登陆就可以访问的方法,多个用逗号分隔
.authorizeRequests()
//其他的需要授权后访问
.anyRequest().authenticated()
.and()
//增加是否登陸过滤
.addFilter(new JWTAuthenticationFilter(authenticationManager()))
// 前后端分离是无状态的,所以暫時不用session,將登陆信息保存在token中。
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
第三步,在增加对方法是否登录进行拦截的过滤器
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.Jwts;
/**
* 是否登陆验证方法
* @author 程就人生
* @date 2019年5月26日
*/
public class JWTAuthenticationFilter extends BasicAuthenticationFilter {
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
/**
* 對請求進行過濾
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) {
try {
//请求体的头中是否包含Authorization
String header = request.getHeader("Authorization");
//Authorization中是否包含Bearer,有一个不包含时直接返回
if (header == null || !header.startsWith("Bearer ")) {
chain.doFilter(request, response);
responseJson(response);
return;
}
//获取权限失败,会抛出异常
UsernamePasswordAuthenticationToken authentication = getAuthentication(request);
//获取后,将Authentication写入SecurityContextHolder中供后序使用
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
} catch (Exception e) {
responseJson(response);
e.printStackTrace();
}
}
/**
* 未登錄時的提示
* @param response
*/
private void responseJson(HttpServletResponse response){
try {
//未登錄時,使用json進行提示
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
PrintWriter out = response.getWriter();
Map<String,Object> map = new HashMap<String,Object>();
map.put("code",HttpServletResponse.SC_FORBIDDEN);
map.put("message","请登录!");
out.write(new ObjectMapper().writeValueAsString(map));
out.flush();
out.close();
} catch (Exception e1) {
e1.printStackTrace();
}
}
/**
* 通过token,获取用户信息
* @param request
* @return
*/
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
String token = request.getHeader("Authorization");
if (token != null) {
//通过token解析出用户信息
String user = Jwts.parser()
//签名盐
.setSigningKey("MyJwtSecret")
.parseClaimsJws(token.replace("Bearer ", ""))
.getBody()
.getSubject();
//不为null,返回
if (user != null) {
return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>());
}
return null;
}
return null;
}
}
从B项目的配置中,可以看出,B项目配置的太简洁了,只需要拦截一下没有登录的请求,连登录也都省了。
A和B项目中分别添加一个Controller,用于测试
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 测试用例
* @author 程就人生
* @date 2019年5月26日
*/
@RestController
public class IndexController {
@GetMapping("/index")
public Object index(){
return "index";
}
}
使用测试工具进行测试
第一步,测试A项目和B项目的index是否能访问,结果都不能访问,测试结果OK


第二步,通过登录获取token,登录成功后,返回了JSON格式的提示,返回的token在头部,点击响应头,获取token


第三步,将token拷贝至A项目index的头部,B项目index的头部,测试结果ok,都可以访问,也可以把token时间设置的短一些,测试一下token过期了,是否还能访问。


最后,感觉一下Token的结构,去掉前面固定的Bearer ,后面的分成三个部分,中间用点隔开,这个就简单了解下吧。
- Header(头部)
- Payload(负载)
- Signature(签名)
Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJmZW5nIiwiZXhwIjoxNTU4OTUxMjM5fQ.X7lOhHJljxnVcNEckYSX22rgTDN0ToRJLaPb_1dAoPzx6q_eN5B5iOxO2GXoNUllIfQG6SrdJhgYzKZPTMsDIg
Spring Security整合JWT,实现单点登录的功能,到此就告一段落了,看起来是不是很简单呢,那就动手试一试吧。
作者:程就人生
链接:https://www.jianshu.com/p/8bd4a6e27e7f
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。
Spring Security整合JWT,实现单点登录,So Easy~!的更多相关文章
- Spring Security 整合freemaker 实现简单登录和角色控制
Spring Security 整合freemaker 实现简单登录和角色控制 写这篇文章是因为我做了一个电商网站项目,近期刚加上权限控制.整个过程很简单,在此给大家梳理一下,也算是自己对知识 ...
- Spring Security 集成CAS实现单点登录
参考:http://elim.iteye.com/blog/2270446 众所周知,Cas是对单点登录的一种实现.本文假设读者已经了解了Cas的原理及其使用,这些内容在本文将不会讨论.Cas有Ser ...
- Spring Security 整合 微信小程序登录的思路探讨
1. 前言 原本打算把Spring Security中OAuth 2.0的机制讲完后,用小程序登录来实战一下,发现小程序登录流程和Spring Security中OAuth 2.0登录的流程有点不一样 ...
- spring boot:spring security整合jwt实现登录和权限验证(spring boot 2.3.3)
一,为什么使用jwt? 1,什么是jwt? Json Web Token, 它是JSON风格的轻量级的授权和身份认证规范, 可以实现无状态.分布式的Web应用授权 2,jwt的官网: https:// ...
- Spring Security 整合JWT(四)
一.前言 本篇文章将讲述Spring Security 简单整合JWT 处理认证授权 基本环境 spring-boot 2.1.8 mybatis-plus 2.2.0 mysql 数据库 maven ...
- spring security集成cas实现单点登录
spring security集成cas 0.配置本地ssl连接 操作记录如下: =====================1.创建证书文件thekeystore ,并导出为thekeystore.c ...
- 总结关于spring security 使用 JWT 和 账户密码登录 整合在一起的新感悟
(1)jwt登录拦截,需要在账户密码认证之前进行jwt认证,因此jwt拦截需要在 UsernamePasswordAuthenticationFilter 之前: (2)jwt验证通过则不需要执行账户 ...
- Spring Boot 集成 JWT 实现单点登录授权
使用步骤如下:1. 添加Gradle依赖: dependencies { implementation 'com.auth0:java-jwt:3.3.0' implementation('org.s ...
- 用Spring Security, JWT, Vue实现一个前后端分离无状态认证Demo
简介 完整代码 https://github.com/PuZhiweizuishuai/SpringSecurity-JWT-Vue-Deom 运行展示 后端 主要展示 Spring Security ...
随机推荐
- CAD二次开发之入门坑
如果没有引用第一个dll,则会报未找到引用CommandMethod
- PHP 根据二维数组中的某个字段进行排序
<?php $data = array( array( 'id' => 5698, 'first_name' => 'Bill', 'last_name' => 'Gates' ...
- python数据写入Excel表格
from openpyxl import Workbook def main(): sheet_name = "表名1" row_count = 6 # 行数 info_resul ...
- Django drf:分页器详解
一.简单分页(查看第n页,每页显示N条) 二.偏移分页(在第n个位置,向后查看n条数据) 三.CursorPagination(加密分页,只能看上一页和下一页,速度快) 一.简单分页(查看第n页,每页 ...
- Netty4实现JTT809对接
网上的使用的netty版本过老,最近自己接触到这一块,重新写了一个 服务器流程 1,判定报文起始和结束标识 ,2去掉头尾标识进行转义,3,去掉CRC码进行CRC计算,4读取报文头,(5,如果加密则解密 ...
- YOLO---多个版本的简单认识
YOLO---多个版本的简单认识 YOLOv3 有好几个经典版本了:一.YOLOv3 (Darknet)官网 @ https://github.com/pjreddie/darknet二.YOLOv3 ...
- 14、RALM: 实时 look-alike 算法在推荐系统中的应用
转载:https://zhuanlan.zhihu.com/p/71951411 RALM: 实时 look-alike 算法在推荐系统中的应用 0. 导语 本论文题为<Real-time At ...
- cmake升级3.6
https://blog.csdn.net/u013714645/article/details/77002555 ./boostrap gmake gmake install
- FFmpeg常用命令学习笔记(七)直播相关命令
直播相关命令 主要涉及到直播中的推流和拉流 1.直播推流 ffmpeg -re -i out.mp4 -c copy -f flv rtmp://server/live/streamName -re: ...
- Java集合--ArrayList,LinkedList性能分析
转载请注明出处:http://www.cnblogs.com/skywang12345/p/3308900.html 第1部分 List概括 先回顾一下List的框架图 (01) List 是一个接口 ...