srpingboot这里就不再进行赘述,相信大家都非常的熟悉了
Shiro
一个安全框架,可以实现用户的认证和授权。比SpringSecurity要简单的多。
Jwt
我的理解就是可以进行客户端与服务端之间验证的一种技术,取代了之前使用 Session 来验证的不安全性
什么不适用 Session?
原理是,登录之后客户端和服务端各自保存一个相应的 SessionId,每次客户端发起请求的时候就得携带这个 SessionId 来进行比对
1、Session 在用户请求量大的时候服务器开销太大了
2、Session 不利于搭建服务器的集群(也就是必须访问原本的那个服务器才能获取对应的 SessionId)
它使用的是一种令牌技术
Jwt 字符串分为三部分
Header
存储两个变量
1、秘钥(可以用来比对)
2、算法(也就是下面将 Header 和 payload 加密成 Signature)
payload
存储很多东西,基础信息有如下几个
1、签发人,也就是这个 “令牌” 归属于哪个用户。一般是userId
2、创建时间,也就是这个令牌是什么时候创建的
3、失效时间,也就是这个令牌什么时候失效
4、唯一标识,一般可以使用算法生成一个唯一标识
Signature
这个是上面两个经过 Header 中的算法加密生成的,用于比对信息,防止篡改 Header 和 payload
然后将这三个部分的信息经过加密生成一个JwtToken的字符串,发送给客户端,客户端保存在本地。当客户端发起请求的时候携带这个到服务端 (可以是在cookie,可以是在header,可以是在localStorage中),在服务端进行验证
pom.xml
由于需要对 shiro 的 SecurityManager 进行设置,所以不能使用 shiro-spring-boot-starter 进行与 springboot 的整合,只能使用 spring-shiro
<!--项目中org.apache.commons.codec.binary.Base64所需-->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.10</version>
</dependency>
<!--java-jwt-核心依赖-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.11.0</version>
</dependency>
<!--jjwt-java版本的辅助帮助模块-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!--shiro相关依赖-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.1</version>
</dependency>
@Configuration
@Slf4j
public class ShiroConfig {
@Bean
public Realm customRealm() {
return new CustomRealm();
}
@Bean
public DefaultWebSecurityManager securityManager(Realm customRealm,SubjectFactory subjectFactory) {
//获取安全管理器
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//在安全管理器中设置Realm
securityManager.setRealm(customRealm);
return securityManager;
}
/**
*
* @param securityManager
* @return
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager){
log.info("进入shiro拦截器");
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
//设置安全管理器
shiroFilter.setSecurityManager(securityManager);
//配置验证失败跳转登录页面的url
shiroFilter.setLoginUrl("/unauthenticated");
//配置授权失败后跳转的url
shiroFilter.setUnauthorizedUrl("/unauthorized");
// 拦截器
Map<String, String> filterRuleMap = new LinkedHashMap<>();
filterRuleMap.put("/login", "anon");
filterRuleMap.put("/logout", "logout");
shiroFilter.setFilterChainDefinitionMap(filterRuleMap);
return shiroFilter;
}
}
@Slf4j
public class CustomRealm extends AuthorizingRealm {
//模拟数据库
//角色表
// tom的角色
private static final Set<String> tomRoleNameSet = new HashSet<>();
// tom的权限
private static final Set<String> tomPermissionNameSet = new HashSet<>();
// jerry的角色
private static final Set<String> jerryRoleNameSet = new HashSet<>();
// jerry的权限
private static final Set<String> jerryPermissionNameSet = new HashSet<>();
static {
tomRoleNameSet.add("admin");
jerryRoleNameSet.add("user");
tomPermissionNameSet.add("user:insert");
tomPermissionNameSet.add("user:update");
tomPermissionNameSet.add("user:delete");
tomPermissionNameSet.add("user:query");
jerryPermissionNameSet.add("user:query");
}
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
log.info("开始进行授权===》{}", principals.getPrimaryPrincipal());
//获取用户名
String userName = (String) principals.getPrimaryPrincipal();
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
if (userName.equals("tom")) {
authorizationInfo.addRoles(tomRoleNameSet);
authorizationInfo.addStringPermissions(tomPermissionNameSet);
}
return authorizationInfo;
}
//身份认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//获取jwt
String userName = (String) token.getPrincipal();
//验证是否存在该用户
if (userName.equals("tom")) {
//验证密码,参数1、用户名,参数2、密码,参数3、自定义Realm名称
return new SimpleAuthenticationInfo(userName, "123456", this.getName());
}
return null;
}
}
public class JwtUtil {
// 生成签名是所使用的秘钥
private final String base64EncodedSecretKey;
// 生成签名的时候所使用的加密算法
private final SignatureAlgorithm signatureAlgorithm;
public JwtUtil(String secretKey, SignatureAlgorithm signatureAlgorithm) {
this.base64EncodedSecretKey = Base64.encodeBase64String(secretKey.getBytes());
this.signatureAlgorithm = signatureAlgorithm;
}
/**
* 生成 JWT Token 字符串
*
* @param iss 签发人名称
* @param ttlMillis jwt 过期时间
* @param claims 额外添加到荷部分的信息。
* 例如可以添加用户名、用户ID、用户(加密前的)密码等信息
*/
public String encode(String iss, long ttlMillis, Map<String, Object> claims) {
if (claims == null) {
claims = new HashMap<>();
}
// 签发时间(iat):荷载部分的标准字段之一
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
// 下面就是在为payload添加各种标准声明和私有声明了
JwtBuilder builder = Jwts.builder()
// 荷载部分的非标准字段/附加字段,一般写在标准的字段之前。
.setClaims(claims)
// JWT ID(jti):荷载部分的标准字段之一,JWT 的唯一性标识,虽不强求,但尽量确保其唯一性。
.setId(UUID.randomUUID().toString())
// 签发时间(iat):荷载部分的标准字段之一,代表这个 JWT 的生成时间。
.setIssuedAt(now)
// 签发人(iss):荷载部分的标准字段之一,代表这个 JWT 的所有者。通常是 username、userid 这样具有用户代表性的内容。
.setSubject(iss)
// 设置生成签名的算法和秘钥
.signWith(signatureAlgorithm, base64EncodedSecretKey);
if (ttlMillis >= 0) {
long expMillis = nowMillis + ttlMillis;
Date exp = new Date(expMillis);
// 过期时间(exp):荷载部分的标准字段之一,代表这个 JWT 的有效期。
builder.setExpiration(exp);
}
return builder.compact();
}
/**
* JWT Token 由 头部 荷载部 和 签名部 三部分组成。签名部分是由加密算法生成,无法反向解密。
* 而 头部 和 荷载部分是由 Base64 编码算法生成,是可以反向反编码回原样的。
* 这也是为什么不要在 JWT Token 中放敏感数据的原因。
*
* @param jwtToken 加密后的token
* @return claims 返回荷载部分的键值对
*/
public Claims decode(String jwtToken) {
// 得到 DefaultJwtParser
return Jwts.parser()
// 设置签名的秘钥
.setSigningKey(base64EncodedSecretKey)
// 设置需要解析的 jwt
.parseClaimsJws(jwtToken)
.getBody();
}
/**
* 校验 token
* 在这里可以使用官方的校验,或,
* 自定义校验规则,例如在 token 中携带密码,进行加密处理后和数据库中的加密密码比较。
*
* @param jwtToken 被校验的 jwt Token
*/
public boolean isVerify(String jwtToken) {
Algorithm algorithm = null;
switch (signatureAlgorithm) {
case HS256:
algorithm = Algorithm.HMAC256(Base64.decodeBase64(base64EncodedSecretKey));
break;
default:
throw new RuntimeException("不支持该算法");
}
JWTVerifier verifier = JWT.require(algorithm).build();
verifier.verify(jwtToken); // 校验不通过会抛出异常
/*
// 得到DefaultJwtParser
Claims claims = decode(jwtToken);
if (claims.get("password").equals(user.get("password"))) {
return true;
}
*/
return true;
}
public static void main(String[] args) {
JwtUtil util = new JwtUtil("tom", SignatureAlgorithm.HS256);
Map<String, Object> map = new HashMap<>();
map.put("username", "tom");
map.put("password", "123456");
map.put("age", 20);
String jwtToken = util.encode("tom", 30000, map);
System.out.println(jwtToken);
/*
util.isVerify(jwtToken);
System.out.println("合法");
*/
util.decode(jwtToken).entrySet().forEach((entry) -> {
System.out.println(entry.getKey() + ": " + entry.getValue());
});
}
}
由于需要对 shiro 的 SecurityManager 进行设置,所以不能使用 shiro-spring-boot-starter 进行与 springboot 的整合,只能使用 spring-shiro
<!-- 自动依赖导入 shiro-core 和 shiro-web -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.1</version>
</dependency>
由于需要实现无状态的 web,所以使用不到 Shiro 的 Session 功能,严谨点就是将其关闭
/**
* 关闭session,因此subject不能使用,创建jwt的subject
*/
public class JwtDefaultSubjectFactory extends DefaultWebSubjectFactory{
@Override
public Subject createSubject(SubjectContext context) {
// 不创建 session,禁止使用session
context.setSessionCreationEnabled(false);
return super.createSubject(context);
}
}
@Component
public class JwtUtil {
//创建默认的秘钥和算法,供无参的构造方法使用
private static final String defaultbase64EncodedSecretKey = "badbabe";
private static final SignatureAlgorithm defaultsignatureAlgorithm = SignatureAlgorithm.HS256;
public JwtUtil() {
this(defaultbase64EncodedSecretKey, defaultsignatureAlgorithm);
}
private final String base64EncodedSecretKey;
private final SignatureAlgorithm signatureAlgorithm;
public JwtUtil(String secretKey, SignatureAlgorithm signatureAlgorithm) {
this.base64EncodedSecretKey = Base64.encodeBase64String(secretKey.getBytes());
this.signatureAlgorithm = signatureAlgorithm;
}
/*
*这里就是产生jwt字符串的地方
* jwt字符串包括三个部分
* 1. header
* -当前字符串的类型,一般都是“JWT”
* -哪种算法加密,“HS256”或者其他的加密算法
* 所以一般都是固定的,没有什么变化
* 2. payload
* 一般有四个最常见的标准字段(下面有)
* iat:签发时间,也就是这个jwt什么时候生成的
* jti:JWT的唯一标识
* iss:签发人,一般都是username或者userId
* exp:过期时间
*
* */
public String encode(String iss, long ttlMillis, Map<String, Object> claims) {
//iss签发人,ttlMillis生存时间,claims是指还想要在jwt中存储的一些非隐私信息
if (claims == null) {
claims = new HashMap<>();
}
long nowMillis = System.currentTimeMillis();
JwtBuilder builder = Jwts.builder()
.setClaims(claims)
.setId(UUID.randomUUID().toString())//2. 这个是JWT的唯一标识,一般设置成唯一的,这个方法可以生成唯一标识
.setIssuedAt(new Date(nowMillis))//1. 这个地方就是以毫秒为单位,换算当前系统时间生成的iat
.setSubject(iss)//3. 签发人,也就是JWT是给谁的(逻辑上一般都是username或者userId)
.signWith(signatureAlgorithm, base64EncodedSecretKey);//这个地方是生成jwt使用的算法和秘钥
if (ttlMillis >= 0) {
long expMillis = nowMillis + ttlMillis;
Date exp = new Date(expMillis);//4. 过期时间,这个也是使用毫秒生成的,使用当前时间+前面传入的持续时间生成
builder.setExpiration(exp);
}
return builder.compact();
}
//相当于encode的方向,传入jwtToken生成对应的username和password等字段。Claim就是一个map
//也就是拿到荷载部分所有的键值对
public Claims decode(String jwtToken) {
// 得到 DefaultJwtParser
return Jwts.parser()
// 设置签名的秘钥
.setSigningKey(base64EncodedSecretKey)
// 设置需要解析的 jwt
.parseClaimsJws(jwtToken)
.getBody();
}
//判断jwtToken是否合法
public boolean isVerify(String jwtToken) {
//这个是官方的校验规则,这里只写了一个”校验算法“,可以自己加
Algorithm algorithm = null;
switch (signatureAlgorithm) {
case HS256:
algorithm = Algorithm.HMAC256(Base64.decodeBase64(base64EncodedSecretKey));
break;
default:
throw new RuntimeException("不支持该算法");
}
JWTVerifier verifier = JWT.require(algorithm).build();
verifier.verify(jwtToken); // 校验不通过会抛出异常
//判断合法的标准:1. 头部和荷载部分没有篡改过。2. 没有过期
return true;
}
public static void main(String[] args) {
JwtUtil util = new JwtUtil("tom", SignatureAlgorithm.HS256);
//以tom作为秘钥,以HS256加密
Map<String, Object> map = new HashMap<>();
map.put("username", "tom");
map.put("password", "123456");
map.put("age", 20);
String jwtToken = util.encode("tom", 30000, map);
System.out.println(jwtToken);
util.decode(jwtToken).entrySet().forEach((entry) -> {
System.out.println(entry.getKey() + ": " + entry.getValue());
});
}
}
在 Shiro 的拦截器中多加一个,等下需要在配置文件中注册这个过滤器
@Configuration
@Slf4j
public class ShiroConfig {
/*
* 告诉shiro不要使用默认的DefaultSubject创建对象,因为不能创建Session
* */
@Bean
public SubjectFactory subjectFactory() {
return new JwtDefaultSubjectFactory();
}
@Bean
public Realm customRealm() {
return new CustomRealm();
}
@Bean
public DefaultWebSecurityManager securityManager(Realm customRealm,SubjectFactory subjectFactory) {
//获取安全管理器
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//在安全管理器中设置Realm
securityManager.setRealm(customRealm);
// 关闭 ShiroDAO 功能
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
// 不需要将 Shiro Session 中的东西存到任何地方(包括 Http Session 中)
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
//禁止Subject的getSession方法
securityManager.setSubjectFactory(subjectFactory);
return securityManager;
}
/**
*
* @param securityManager
* @return
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager){
log.info("进入shiro拦截器");
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
//设置安全管理器
shiroFilter.setSecurityManager(securityManager);
//配置验证失败跳转登录页面的url
shiroFilter.setLoginUrl("/unauthenticated");
//配置授权失败后跳转的url
shiroFilter.setUnauthorizedUrl("/unauthorized");
/*
* 添加jwt过滤器,并在下面注册
* 也就是将jwtFilter注册到shiro的Filter中
* 指定除了login和logout之外的请求都先经过jwtFilter
* */
Map<String, Filter> filterMap = new HashMap<>();
//这个地方其实另外两个filter可以不设置,默认就是
filterMap.put("anon", new AnonymousFilter());
filterMap.put("jwt", new JwtFilter());
filterMap.put("logout", new LogoutFilter());
shiroFilter.setFilters(filterMap);
// 拦截器
Map<String, String> filterRuleMap = new LinkedHashMap<>();
filterRuleMap.put("/login", "anon");
filterRuleMap.put("/logout", "logout");
//swagger放行
filterRuleMap.put("/swagger-ui.html", "anon");
filterRuleMap.put("/swagger-resources", "anon");
filterRuleMap.put("/v2/api-docs", "anon");
filterRuleMap.put("/webjars/springfox-swagger-ui/**", "anon");
filterRuleMap.put("/configuration/security", "anon");
filterRuleMap.put("/configuration/ui", "anon");
//任何请求都需要经过jwt过滤器
filterRuleMap.put("/**", "jwt");
shiroFilter.setFilterChainDefinitionMap(filterRuleMap);
return shiroFilter;
}
}
用于在customRealm中用token获取Principal
public class JwtToken implements AuthenticationToken {
private String jwt;
public JwtToken(String jwt) {
this.jwt = jwt;
}
@Override//类似是用户名
public Object getPrincipal() {
return jwt;
}
@Override//类似密码
public Object getCredentials() {
return jwt;
}
//返回的都是jwt
}
创建判断jwt是否有效的认证方式的Realm
@Slf4j
public class JwtRealm extends AuthorizingRealm {
/*
* 多重写一个support
* 标识这个Realm是专门用来验证JwtToken
* 不负责验证其他的token(UsernamePasswordToken)
* */
@Override
public boolean supports(AuthenticationToken token) {
//这个token就是从过滤器中传入的jwtToken
return token instanceof JwtToken;
}
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
//认证
//这个token就是从过滤器中传入的jwtToken
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String jwt = (String) token.getPrincipal();
if (jwt == null) {
throw new NullPointerException("jwtToken 不允许为空");
}
JwtUtil jwtUtil = new JwtUtil();
//验证jwt是否合法
if (!jwtUtil.isVerify(jwt)) {
throw new UnknownAccountException();
}
//下面是验证这个user是否是真实存在的
String username = (String) jwtUtil.decode(jwt).get("username");//获取jwt中的用户名
log.info("在使用token登录"+username);
return new SimpleAuthenticationInfo(jwt,jwt,"JwtRealm");
//这里返回的是类似账号密码的东西,但是jwtToken都是jwt字符串。还需要一个该Realm的类名
}
}
配置信息
1、因为不适用 Session,所以为了防止会调用 getSession() 方法而产生错误,所以默认调用自定义的 Subject 方法
2、一些修改,关闭 SHiroDao 等
3、注册 JwtFilter
@Configuration
@Slf4j
public class ShiroConfig {
/*
* 告诉shiro不要使用默认的DefaultSubject创建对象,因为不能创建Session
* */
@Bean
public SubjectFactory subjectFactory() {
return new JwtDefaultSubjectFactory();
}
@Bean
public Realm customRealm() {
return new CustomRealm();
}
@Bean
public DefaultWebSecurityManager securityManager(Realm customRealm,SubjectFactory subjectFactory) {
//获取安全管理器
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//在安全管理器中设置Realm
securityManager.setRealm(customRealm);
// 关闭 ShiroDAO 功能
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
// 不需要将 Shiro Session 中的东西存到任何地方(包括 Http Session 中)
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
//禁止Subject的getSession方法
securityManager.setSubjectFactory(subjectFactory);
return securityManager;
}
/**
*
* @param securityManager
* @return
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager){
log.info("进入shiro拦截器");
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
//设置安全管理器
shiroFilter.setSecurityManager(securityManager);
//配置验证失败跳转登录页面的url
shiroFilter.setLoginUrl("/unauthenticated");
//配置授权失败后跳转的url
shiroFilter.setUnauthorizedUrl("/unauthorized");
/*
* 添加jwt过滤器,并在下面注册
* 也就是将jwtFilter注册到shiro的Filter中
* 指定除了login和logout之外的请求都先经过jwtFilter
* */
Map<String, Filter> filterMap = new HashMap<>();
//这个地方其实另外两个filter可以不设置,默认就是
filterMap.put("anon", new AnonymousFilter());
filterMap.put("jwt", new JwtFilter());
filterMap.put("logout", new LogoutFilter());
shiroFilter.setFilters(filterMap);
// 拦截器
Map<String, String> filterRuleMap = new LinkedHashMap<>();
filterRuleMap.put("/login", "anon");
filterRuleMap.put("/logout", "logout");
//swagger放行
filterRuleMap.put("/swagger-ui.html", "anon");
filterRuleMap.put("/swagger-resources", "anon");
filterRuleMap.put("/v2/api-docs", "anon");
filterRuleMap.put("/webjars/springfox-swagger-ui/**", "anon");
filterRuleMap.put("/configuration/security", "anon");
filterRuleMap.put("/configuration/ui", "anon");
//任何请求都需要经过jwt过滤器
filterRuleMap.put("/**", "jwt");
shiroFilter.setFilterChainDefinitionMap(filterRuleMap);
return shiroFilter;
}
}
@Slf4j
@Controller
public class LoginController {
/**
* 用户登录
* 不走jwt的过滤器,但是在这个时候生产jwt,方便后面访问接口时使用
* @param username
* @param password
* @return
*/
@RequestMapping("/login")
public ResponseEntity<Map<String, String>> login(String username, String password) {
log.info("username:{},password:{}", username, password);
Map<String, String> map = new HashMap<>();
if (!"tom".equals(username) || !"123".equals(password)) {
map.put("msg", "用户名密码错误");
return ResponseEntity.ok(map);
}
JwtUtil jwtUtil = new JwtUtil();
Map<String, Object> chaim = new HashMap<>();
chaim.put("username", username);
chaim.put("password",password);
//获取jwt,5 * 60 * 1000代表5分钟,
String jwtToken = jwtUtil.encode(username, 100 * 60 * 1000, chaim);
map.put("msg", "登录成功");
map.put("token", jwtToken);
return ResponseEntity.ok(map);
}
@RequestMapping("/logout")
public ResponseEntity<String> logout(){
return ResponseEntity.ok("退出成功");
}
/**
* 需要走jwt过滤器判断是否有jwt
* @return
*/
@RequestMapping("/testdemo")
public ResponseEntity<String> testdemo() {
return ResponseEntity.ok("你好吗");
}
}