Spring Seccurity & Shiro
JWT的原理和流程
一.流程说明:
-
浏览器发起请求登陆,携带用户名和密码;
-
服务端验证身份,根据算法,将用户标识符打包生成 token,
-
服务器返回token(JWT信息)给浏览器,JWT不包含敏感信息;
-
浏览器发起请求获取用户资料,把刚刚拿到的token一起发送给服务器;
-
服务器发现数据中有 token,验明正身;
-
服务器返回该用户的用户资料;
二.JWT消息构成
一个token分3部分,按顺序:
-
头部(header)
1.声明类型,这里是jwt
2.声明加密的算法 通常直接使用 HMAC SHA256
-
载荷(payload) Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。
-
签证(signature) 对象为一个很长的字符串,字符之间通过"."分隔符分为三个子串。注意JWT对象为一个长字串,各字串之间也没有换行符,一般格式为:xxxxx.yyyyy.zzzzz 。
三.JWT 的原理
JWT 的原理是,服务器认证以后,生成一个 JSON 对象,返回给用户,就像下面这样。
{ "姓名": "张三",
"角色": "管理员",
"到期时间": "2018年7月1日0点0分" }
以后,用户与服务端通信的时候,都要返回这个 JSON 对象。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名。
服务器就不保存任何 session 数据了,也就是说,服务器变成无状态了,从而比较容易实现扩展。
区别
(1) session 存储在服务端占用服务器资源,而 JWT 存储在客户端
(2) session 存储在 Cookie 中,存在伪造跨站请求伪造攻击的风险
(3) session 只存在一台服务器上,那么下次请求就必须请求这台服务器,不利于分布式应用
(4) 存储在客户端的 JWT 比存储在服务端的session 更具有扩展性
Spring security的简单原理:
SpringSecurity有很多很多的拦截器,在执行流程里面主要有两个核心的拦截器
登陆验证拦截器AuthenticationProcessingFilter
资源管理拦截器AbstractSecurityInterceptor
但拦截器里面的实现需要一些组件来实现,所以就有了AuthenticationManager认证管理器、accessDecisionManager决策管理器等组件来支撑。
FilterChainProxy是一个代理,真正起作用的是各个Filter,这些Filter作为Bean被Spring管理,是Spring Security核心,各有各的职责,不直接处理认证和授权,交由认证管理器和决策管理器处理!
大概流程
认证管理
流程图解读:
1、用户提交用户名、密码被SecurityFilterChain中的 UsernamePasswordAuthenticationFilter 过滤器获取到, 封装为请求Authentication,通常情况下是UsernamePasswordAuthenticationToken这个实现类。
2、然后过滤器将Authentication提交至认证管理器(AuthenticationManager)进行认证 。
3、认证成功后,AuthenticationManager 身份管理器返回一个被填充满了信息的(包括上面提到的权限信息, 身份信息,细节信息,但密码通常会被移除) Authentication 实例。
4、SecurityContextHolder安全上下文容器将第3步填充了信息的Authentication ,通过 SecurityContextHolder.getContext().setAuthentication(…)方法,设置到其中。可以看出AuthenticationManager接口(认证管理器)是认证相关的核心接口,也是发起认证的出发点,它的实现类为ProviderManager。而Spring Security支持多种认证方式,因此ProviderManager维护着一个 List 列表,存放多种认证方式,最终实际的认证工作是由 AuthenticationProvider完成的。咱们知道web表单的对应的AuthenticationProvider实现类为DaoAuthenticationProvider,它的内部又维护着一个UserDetailsService负责UserDetails的获取。最终 AuthenticationProvider将UserDetails填充至Authentication。
授权管理
访问资源(即授权管理),访问url时,会通过FilterSecurityInterceptor拦截器拦截,其中会调用SecurityMetadataSource的方法来获取被拦截url所需的全部权限,再调用授权管理器AccessDecisionManager,这个授权管理器会通过spring的全局缓存SecurityContextHolder获取用户的权限信息,还会获取被拦截的url和被拦截url所需的全部权限,然后根据所配的投票策略(有:一票决定,一票否定,少数服从多数等),如果权限足够,则决策通过,返回访问资源,请求放行,否则跳转到403页面、自定义页面。
SecurityConfig文件详解
HttpSecurity 和WebSecurity与Web 相关,AuthenticationManagerBuilder与验证相关
AuthenticationManagerBuilder : 用来配置全局的验证资讯,也就是AuthenticationProvider 和UserDetailsService。
WebSecurity : 用来配置全局忽略的规则,如静态资源、是否Debug、全局的HttpFirewall、SpringFilterChain 配置、privilegeEvaluator、expressionHandler、securityInterceptor,启动HTTPS等
HttpSecurity : 用来配置各种具体的验证机制规则,如OpenIDLoginConfigurer、AnonymousConfigurer、FormLoginConfigurer、HttpBasicConfigurer 等。
SpringBoot 整合Seccurity认证授权
SpringSecurity+Jwt做前后端分离权限认证_jwtloginfilter_雾晴的博客-CSDN博客
第一步:引入所需maven 依赖
<!-- security架包 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--引入thymeleaf与Spring Security整合的依赖-->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
<!--jwt依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
第二步: 配置JWT过滤器
所有的请求都会先经过过滤器 (除配置了白名单的)
这里拿到请求中携带的token,将token中权限部分取出来,然后保存到Security的Authentication授权管理器中,给我们后面注入那个AuthenticationProviderImpl,
package com.shangdi.jigui.filter;
import com.shangdi.jigui.entity.LoginUser;
import com.shangdi.jigui.utils.JwtUtil;
import com.shangdi.jigui.utils.RedisCache;
import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Objects;
/**
* <p>
* JWT过滤器
* </p>
*
* @author shangdi
*/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private RedisCache redisCache;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//获取token
String token = request.getHeader("token");
if (!StringUtils.hasText(token)) {
//放行
filterChain.doFilter(request, response);
return;
}
//解析token
String userid;
try {
Claims claims = JwtUtil.parseJWT(token);
userid = claims.getSubject();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("token非法");
}
//从redis中获取用户信息
String redisKey = "login:" + userid;
LoginUser loginUser = redisCache.getCacheObject(redisKey);
if (Objects.isNull(loginUser)) {
throw new RuntimeException("用户未登录");
}
//存入SecurityContextHolder
//TODO 获取权限信息封装到Authentication中
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser,null, loginUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//放行
filterChain.doFilter(request,response);
}
}
第三步:配置SecurityConfig
package com.shangdi.jigui.config;
import com.shangdi.jigui.filter.JwtAuthenticationTokenFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.annotation.Resource;
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true) //基于注解的权限控制方案
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
public SecurityConfig() {
}
//配置采用哪种密码加密算法
@Bean
public PasswordEncoder passwordEncoder() {
// return new BCryptPasswordEncoder(); // 使用BCrypt加密密码
// return NoOpPasswordEncoder.getInstance(); //密码不加密
//strength=10,即密钥的迭代次数(strength取值在4~31之间,默认为10)
//return new BCryptPasswordEncoder(10);
//利用工厂类PasswordEncoderFactories实现,工厂类内部采用的是委派密码编码方案.
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
/**
* 解决 无法直接注入 AuthenticationManager
*
* @return
* @throws Exception
*/
@Bean //暴露authenticationManager
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//关闭csrf
.csrf().disable()
//不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
//登录失败的页面 //也是post请求
// 对于登录接口 允许匿名访问
.antMatchers("/toLogin", "/user/*", "/swagger-ui.html/**", "/swagger-resources/**", "/webjars/**", "/v2/**", "/api/**", "/doc.html").anonymous()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
//允许跨域
http.cors();
//添加过滤器
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
}
创建 JWT工具类
package com.shangdi.jigui.utils;
import io.jsonwebtoken.*;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.UUID;
/**
* JWT工具类
*
* @author shangdi
*/
public class JwtUtil {
//有效期为
public static final Long JWT_TTL = 60 * 60 * 1000L;// 60 * 60 *1000 一个小时
//设置秘钥明文
public static final String JWT_KEY = "hyj";
public static String getUUID() {
String token = UUID.randomUUID().toString().replaceAll("-", "");
return token;
}
/**
* 生成jwt
*
* @param subject token中要存放的数据(json格式)
* @return
*/
public static String createJWT(String subject) {
JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间
return builder.compact();
}
/**
* 生成jwt
*
* @param subject token中要存放的数据(json格式)
* @param ttlMillis token超时时间
* @return
*/
public static String createJWT(String subject, Long ttlMillis) {
JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间
return builder.compact();
}
private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
SecretKey secretKey = generalKey();
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
if (ttlMillis == null) {
ttlMillis = JwtUtil.JWT_TTL;
}
long expMillis = nowMillis + ttlMillis;
Date expDate = new Date(expMillis);
return Jwts.builder()
.setId(uuid) //唯一的ID
.setSubject(subject) // 主题 可以是JSON数据
.setIssuer("sg") // 签发者
.setIssuedAt(now) // 签发时间
.signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥
.setExpiration(expDate);
}
/**
* 创建token
*
* @param id
* @param subject
* @param ttlMillis
* @return
*/
public static String createJWT(String id, String subject, Long ttlMillis) {
JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间
return builder.compact();
}
public static void main(String[] args) throws Exception {
//解密
String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIwZGI3MjIwYTY3MzQ0NGQ2ODYyYTBjOWY3YmMxYjhkNCIsInN1YiI6IjIiLCJpc3MiOiJzZyIsImlhdCI6MTY3ODI0MTY0MywiZXhwIjoxNjc4MjQ1MjQzfQ.8tji5vArlcdrjLujWUbMcAEPpW5eWGk1HqfSkPZaQh4";
String subject = parseJWT(token).getSubject();
System.out.println(subject);
//加密
/*String jwt = createJWT("1");
System.out.println(jwt);*/
}
/**
* 生成加密后的秘钥 secretKey
*
* @return
*/
public static SecretKey generalKey() {
byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return key;
}
/**
* 解析
*
* @param jwt
* @return
* @throws Exception
*/
public static Claims parseJWT(String jwt) throws Exception {
SecretKey secretKey = generalKey();
Claims claims;
try {
claims = Jwts.parser()
.setSigningKey(secretKey) // 设置标识名
.parseClaimsJws(jwt) //解析token
.getBody();
} catch (ExpiredJwtException e) {
claims = e.getClaims();
}
return claims;
}
}
登录接口实现
package com.shangdi.jigui.service.impl;
import com.shangdi.jigui.entity.LoginUser;
import com.shangdi.jigui.entity.ResponseResult;
import com.shangdi.jigui.service.LoginService;
import com.shangdi.jigui.utils.JwtUtil;
import com.shangdi.jigui.utils.RedisCache;
import org.apache.catalina.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
/**
* <p>
* 用户登录实现接口
* </p>
*
*/
@Service
public class LoginServiceImpl implements LoginService {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private RedisCache redisCache;
@Override
public ResponseResult login(String username,String password) {
// 用户验证
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username,password);
// 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
//使用userid生成token
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
String userId = loginUser.getUser().getUserId().toString();
String jwt = JwtUtil.createJWT(userId);
Map<String, String> map = new HashMap<>();
map.put("token", jwt);
//把完整的用户信息存入redis,userid作为key
redisCache.setCacheObject("login:" + userId, loginUser);
return new ResponseResult(200, "登录成功!!", map);
}
@Override
public ResponseResult loginOut() {
//获取SecurityContextHolder中的用户id
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
int id = loginUser.getUser().getUserId();
redisCache.deleteObject("login:" + id);
return new ResponseResult(200, "退出成功!!");
}
}
实现 org.springframework.security.core.userdetails.UserDetailsService 接口
重写 loadUserByUsername 方法
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Service;
/**
* <p>
* 用户查询接口
* </p>
*
* @author shangdi
* @since 2023-03-10
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Autowired
private UserRoleService userRoleService;
@Autowired
private RolePermissionService rolePerService;
@Override
public UserDetails loadUserByUsername(String username) {
//查询用户信息
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("user_name", username);
//如果没有查询到用户
User user = userMapper.selectOne(wrapper);
if (Objects.isNull(user)) {
throw new InternalAuthenticationServiceException("用户名不存在!!");
}
//定义权限列表
Set<String> pers = new HashSet<>();
//获取用户角色集合
List<Role> roles = userRoleService.queryByUId(user.getUserId());
for (Role role : roles) {
//拿到用户具有的权限
List<Permission> permissions = rolePerService.queryByRId(role.getRid());
for (Permission permission : permissions) {
pers.add(permission.getStr());
}
}
return new LoginUser(user, pers);
}
用户请求登录接口,SecurityConfig白名单放行。
效验用户名密码,效验成功,获取用户信息,如果redis缓存中有用户信息,就直接返回用户信息,
否则就查询返回用户信息并存入redis,根据用户信息生成token并返回给前端,每次请求时,JwtAuthenticationFilter
会判断是否携带token,需携带token才能放行。
SpringSecurity+JWT认证流程解析-CSDN博客
前端vue3+token实现用户认证
接收并存储后端返回的token 数据
1.新建一个auth.js (设置Token存储方法)
import Cookies from 'js-cookie'
const TokenKey = 'Admin-Token'
export function getToken() {
return Cookies.get(TokenKey)
}
export function setToken(token) {
return Cookies.set(TokenKey, token, {expires: 300})
}
export function removeToken() {
return Cookies.remove(TokenKey)
}
2.
3.request.js
import axios from 'axios'
import {getToken} from '@/utils/auth'
import errorCode from '@/utils/errorCode'
// import {tansParams} from '@/utils/ruoyi'
import NProgress from 'nprogress'
import {ElNotification} from "element-plus";
export const baseURL = import.meta.env.VITE_APP_BASE_API;
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
// 创建axios实例
const service = axios.create({
// axios中请求配置有baseURL选项,表示请求URL公共部分
baseURL: baseURL, // 超时 默认 3000
timeout: 5000
})
// request拦截器
service.interceptors.request.use(config => {
NProgress.start()
// 是否需要设置 token
const isToken = (config.headers || {}).isToken === false
if (getToken() && !isToken) {
config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
}
// get请求映射params参数
if (config.method === 'get' && config.params) {
let url = config.url + '?' + tansParams(config.params);
url = url.slice(0, -1);
config.params = {};
config.url = url;
}
return config
}, error => {
NProgress.done()
Promise.reject(error)
})
// 响应拦截器
service.interceptors.response.use(res => {
// 未设置状态码则默认成功状态
const code = res.data.code;
// 获取错误信息
const msg = errorCode[code] || res.data.msg || errorCode['default']
// 二进制数据则直接返回
if (res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer') {
return res.data
}
NProgress.done()
if (code !== 200) {
ElNotification.error(msg)
return Promise.reject(res.data)
} else {
return res;
}
}, error => {
console.log('err' + error)
let {message} = error;
if (message === "Network Error") {
message = "后端接口连接异常";
} else if (message.includes("timeout")) {
message = "系统接口请求超时";
} else if (message.includes("Request failed with status code")) {
message = "系统接口" + message.substr(message.length - 3) + "异常";
}
ElNotification.error(message)
NProgress.done()
return Promise.reject(error)
})
export const POST = (url, data, options) => service({url, method: 'post', data, ...options});
export const DELETE = (url, params, options) => service({url, method: 'delete', params, ...options});
export const GET = (url, params, options) => service({url, method: 'get', params, ...options});
export const PUT = (url, data, options) => service({url, method: 'put', data, ...options});
export default service
请求效果如下: