Shiro和JWT
Shiro
1 权限的管理
1.1 什么是权限管理
基本上涉及到用户参与的系统都要进行权限管理,权限管理属于
系统安全的范畴,权限管理实现对用户访问系统的控制,按照安
全规则或者[安全策略]
(http://baike.baidu.com/view/160028.htm)控制用户可以访问而且只能访问自己被授权的资源。
权限管理包括用户身份认证和授权两部分,简称认证授权。对于
需要访问控制的资源用户首先经过身份认证,认证通过后用户
具有该资源的访问权限方可访问。
1.2 什么是身份认证
身份认证,就是判断一个用户是否为合法用户的处理过程。
最常用的简单身份认证方式是系统通过核对用户输入的用
户名和口令,看其是否与系统中存储的该用户的用户名和
口令一致,来判断用户身份是否正确。对于采用指纹等
系统,则出示指纹;对于硬件Key等刷卡系统,则需要刷卡。
1.3 什么是授权
授权,即访问控制,控制谁能访问哪些资源。主体进行身份认
证后需要分配权限方可访问系统的资源,对于某些资源没有
权限是无法访问的
2 什么是shiro
Apache Shiro™ is a powerful and easy-to-use Java
security framework that performs authentication,
authorization, cryptography, and session management.
With Shiro’s easy-to-understand API, you can
quickly and easily secure any application – from the
smallest mobile applications to the largest web and
enterprise applications.
Shiro 是一个功能强大且易于使用的Java安全框架,它执行
身份验证、授权、加密和会话管理。使用Shiro易于理解的API,
您可以快速轻松地保护任何应用程序—从最小的移动应用程序到最
大的web和企业应用程序。
Shiro是apache旗下一个开源框架,它将软件系统的安全认证相关
的功能抽取出来,实现用户身份认证,权限授权、加密、会话管
理等功能,组成了一个通用的安全认证框架。
3 shiro的核心架构
3.1 Subject
Subject即主体,外部应用与subject进行交互,subject记录了当前
操作用户,将用户的概念理解为当前操作的主体,可能是一个通过浏
览器请求的用户,也可能是一个运行的程序。 Subject在shiro中
是一个接口,接口中定义了很多认证授相关的方法,外部程序通
过subject进行认证授,而subject是通过SecurityManager安全
管理器进行认证授权
3.2 SecurityManager
SecurityManager即安全管理器,对全部的subject进行安全管理,
它是shiro的核心,负责对所有的subject进行安全管理。通过
SecurityManager可以完成subject的认证、授权等,实质上
SecurityManager是通过Authenticator进行认证,通过Authorizer进
行授权,通过SessionManager进行会话管理等。
SecurityManager是一个接口,继承了Authenticator, Authorizer,
SessionManager这三个接口。
3.3 Authenticator
Authenticator即认证器,对用户身份进行认证,Authenticator是
一个接口,shiro提供ModularRealmAuthenticator实现类,通过
ModularRealmAuthenticator基本上可以满足大多数需求,也可以
自定义认证器。
3.4 Authorizer
Authorizer即授权器,用户通过认证器认证通过,在访问功能时
需要通过授权器判断用户是否有此功能的操作权限。
3.5 Realm
Realm即领域,相当于datasource数据源,securityManager进行
安全认证需要通过Realm获取用户权限数据,比如:如果用户
身份数据在数据库那么realm就需要从数据库获取用户身份信息。
注意:不要把realm理解成只是从数据源取数据,在realm中还
有认证授权校验的相关的代码。
3.6 SessionManager
sessionManager即会话管理,shiro框架定义了一套会话管理,
它不依赖web容器的session,所以shiro可以使用在非web
应用上,也可以将分布式应用的会话集中在一点管理,此
特性可使它实现单点登录。
3.7 SessionDAO
SessionDAO即会话dao,是对session会话操作的一套接
口,比如要将session存储到数据库,可以通过jdbc将会
话存储到数据库。
3.8 CacheManager
CacheManager即缓存管理,将用户权限数据存储在缓存,
这样可以提高性能。
3.9 Cryptography
Cryptography即密码管理,shiro提供了一套加密/解密的组件,
方便开发。比如提供常用的散列、加/解密等功能。
4 shiro中的认证
4.1 认证
身份认证,就是判断一个用户是否为合法用户的处理过程。最
常用的简单身份认证方式是系统通过核对用户输入的用户名和口
令,看其是否与系统中存储的该用户的用户名和口令一致,来判
断用户身份是否正确。
4.2 shiro中认证的关键对象
Subject:主体
访问系统的用户,主体可以是用户、程序等,进行认证的都称为主体;
Principal:身份信息
是主体(subject)进行身份认证的标识,标识必须具有唯一性,
如用户名、手机号、邮箱地址等,一个主体可以有多个身份,
但是必须有一个主身份(Primary Principal)。
credential:凭证信息
是只有主体自己知道的安全信息,如密码、证书等。
4.3 认证流程
4.4 认证的开发
4.4.1 创建项目并引入依赖
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-coreartifactId>
<version>1.5.3version>
dependency>
4.4.2 引入shiro配置文件并加入如下配置
[users]
xiaochen=123
zhangsan=456
4.4.3 开发认证代码
package com.baizhi;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.text.IniRealm;
import org.apache.shiro.subject.Subject;
public class TestAuthenticator {
public static void main(String[] args) {
DefaultSecurityManager securityManager = new DefaultSecurityManager();
securityManager.setRealm(new IniRealm("classpath:shiro.ini"));
SecurityUtils.setSecurityManager(securityManager);
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("xiaochen","123");
try{
System.out.println("认证状态: "+ subject.isAuthenticated());
subject.login(token);
System.out.println("认证状态: "+ subject.isAuthenticated());
}catch (UnknownAccountException e){
e.printStackTrace();
System.out.println("认证失败: 用户名不存在~");
}catch (IncorrectCredentialsException e){
e.printStackTrace();
System.out.println("认证失败: 密码错误~");
}
}
}
DisabledAccountException(帐号被禁用)
LockedAccountException(帐号被锁定)
ExcessiveAttemptsException(登录失败次数过多)
ExpiredCredentialsException(凭证过期)等
4.5 自定义Realm
上边的程序使用的是Shiro自带的IniRealm,IniRealm从ini配置
文件中读取用户的信息,大部分情况下需要从系统的数据库中读
取用户信息,所以需要自定义realm。
4.5.1 shiro提供的Realm
4.5.2 根据认证源码认证使用的是SimpleAccountRealm
SimpleAccountRealm的部分源码中有两个方法一个是认证 一
个是授权
public class SimpleAccountRealm extends AuthorizingRealm {
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
SimpleAccount account = getUser(upToken.getUsername());
if (account != null) {
if (account.isLocked()) {
throw new LockedAccountException("Account [" + account + "] is locked.");
}
if (account.isCredentialsExpired()) {
String msg = "The credentials for account [" + account + "] are expired";
throw new ExpiredCredentialsException(msg);
}
}
return account;
}
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username = getUsername(principals);
USERS_LOCK.readLock().lock();
try {
return this.users.get(username);
} finally {
USERS_LOCK.readLock().unlock();
}
}
}
4.5.3 自定义realm
package com.baizhi.realm;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
public class CustomerRealm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
System.out.println("==================");
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String principal = (String) token.getPrincipal();
System.out.println(principal);
if("xiaochen".equals(principal)){
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(principal,"123",this.getName());
return simpleAuthenticationInfo;
}
return null;
}
}
4.5.4 测试
package com.baizhi;
import com.baizhi.realm.CustomerRealm;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.subject.Subject;
public class TestCustomerRealmAuthenticator {
public static void main(String[] args) {
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
defaultSecurityManager.setRealm(new CustomerRealm());
SecurityUtils.setSecurityManager(defaultSecurityManager);
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("xiaochen", "123");
try {
subject.login(token);
System.out.println(subject.isAuthenticated());
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户名错误");
} catch (IncorrectCredentialsException e) {
e.printStackTrace();
System.out.println("密码错误");
}
if(subject.isAuthenticated()){
System.out.println(subject.hasRole("admin"));
}
}
}
4.6 使用MD5和Salt
实际应用是将盐和散列后的值存在数据库中,自动realm从数据库取
出盐和加密后的值由shiro完成密码校验。
package com.baizhi;
import org.apache.shiro.crypto.hash.Md5Hash;
public class TestShiroMD5 {
public static void main(String[] args) {
Md5Hash md5Hash = new Md5Hash("123");
System.out.println(md5Hash.toHex());
Md5Hash md5Hash1 = new Md5Hash("123", "X0*7ps");
System.out.println(md5Hash1.toHex());
Md5Hash md5Hash2 = new Md5Hash("123", "X0*7ps", 1024);
System.out.println(md5Hash2.toHex());
}
}
4.6.1 自定义md5+salt的realm
package com.baizhi.realm;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
public class CustomerMd5Realm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String primaryPrincipal = (String) principals.getPrimaryPrincipal();
System.out.println("身份信息: "+primaryPrincipal);
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.addRole("admin");
simpleAuthorizationInfo.addRole("user");
simpleAuthorizationInfo.addStringPermission("user:*:01");
simpleAuthorizationInfo.addStringPermission("product:create");
return simpleAuthorizationInfo;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String principal = (String) token.getPrincipal();
if ("xiaochen".equals(principal)) {
return new SimpleAuthenticationInfo(principal,
"e4f9bf3e0c58f045e62c23c533fcf633",
ByteSource.Util.bytes("X0*7ps"),
this.getName());
}
return null;
}
}
4.6.2 测试
package com.baizhi;
import com.baizhi.realm.CustomerMd5Realm;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.subject.Subject;
import java.util.Arrays;
public class TestCustomerMd5RealmAuthenicator {
public static void main(String[] args) {
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
CustomerMd5Realm realm = new CustomerMd5Realm();
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
credentialsMatcher.setHashAlgorithmName("md5");
credentialsMatcher.setHashIterations(1024);
realm.setCredentialsMatcher(credentialsMatcher);
defaultSecurityManager.setRealm(realm);
SecurityUtils.setSecurityManager(defaultSecurityManager);
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("xiaochen", "123");
try {
subject.login(token);
System.out.println("登录成功");
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户名错误");
}catch (IncorrectCredentialsException e){
e.printStackTrace();
System.out.println("密码错误");
}
if(subject.isAuthenticated()){
System.out.println(subject.hasRole("super"));
System.out.println(subject.hasAllRoles(Arrays.asList("admin", "super")));
boolean[] booleans = subject.hasRoles(Arrays.asList("admin", "super", "user"));
for (boolean aBoolean : booleans) {
System.out.println(aBoolean);
}
System.out.println("==============================================");
System.out.println("权限:"+subject.isPermitted("user:update:01"));
System.out.println("权限:"+subject.isPermitted("product:create:02"));
boolean[] permitted = subject.isPermitted("user:*:01", "order:*:10");
for (boolean b : permitted) {
System.out.println(b);
}
boolean permittedAll = subject.isPermittedAll("user:*:01", "product:create:01");
System.out.println(permittedAll);
}
}
}
5 shiro中的授权
5.1 授权
授权,即访问控制,控制谁能访问哪些资源。主体进行身份认
证后需要分配权限方可访问系统的资源,对于某些资源没有权
限是无法访问的。
5.2 关键对象
授权可简 单理解为who对what(which)进行How操作:
Who,即主体(Subject),主体需要访问系统中的资源。
What,即资源(Resource),如系统菜单、页面、按钮、类方法、
系统商品信息等。资源包括资源类型和资源实例,比如商品信息
为资源类型,类型为t01的商品为资源实例,编号为001的商品信息
也属于资源实例。
How,权限/许可(Permission),规定了主体对资源的操作许可,
权限离开资源没有意义,如用户查询权限、用户添加权限、某个
类方法的调用权限、编号为001用户的修改权限等,通过权限可
知主体对哪些资源都有哪些操作许可。
5.3 授权流程
5.4 授权方式
基于角色的访问控制
RBAC基于角色的访问控制(Role-Based Access Control)是以
角色为中心进行访问控制
if(subject.hasRole("admin")){
}
基于资源的访问控制
RBAC基于资源的访问控制(Resource-Based Access Control)
是以资源为中心进行访问控制
if(subject.isPermission("user:update:01")){
}
if(subject.isPermission("user:update:*")){
}
5.5 权限字符串
权限字符串的规则是:资源标识符:操作:资源实例标识符,
意思是对哪个资源的哪个实例具有什么操作,“:”是资源/操作/实
例的分割符,权限字符串也可以使用*通配符。
例子:
用户创建权限:user:create,或user:create:*
用户修改实例001的权限:user:update:001
用户实例001的所有权限:user:*:001
5.6 shiro中授权编程实现方式
编程式
Subject subject = SecurityUtils.getSubject();
if(subject.hasRole(“admin”)) {
} else {
}
注解式
@RequiresRoles("admin")
public void hello() {
}
标签式
JSP/GSP 标签:在JSP/GSP 页面通过相应的标签完成:
注意: Thymeleaf 中使用shiro需要额外集成!
5.7 开发授权
5.7.1 realm的实现
package com.baizhi.realm;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
public class CustomerMd5Realm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String primaryPrincipal = (String) principals.getPrimaryPrincipal();
System.out.println("身份信息: "+primaryPrincipal);
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.addRole("admin");
simpleAuthorizationInfo.addRole("user");
simpleAuthorizationInfo.addStringPermission("user:*:01");
simpleAuthorizationInfo.addStringPermission("product:create");
return simpleAuthorizationInfo;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String principal = (String) token.getPrincipal();
if ("xiaochen".equals(principal)) {
return new SimpleAuthenticationInfo(principal,
"e4f9bf3e0c58f045e62c23c533fcf633",
ByteSource.Util.bytes("X0*7ps"),
this.getName());
}
return null;
}
}
5.7.2 测试
package com.baizhi;
import com.baizhi.realm.CustomerMd5Realm;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.subject.Subject;
import java.util.Arrays;
public class TestCustomerMd5RealmAuthenicator {
public static void main(String[] args) {
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
CustomerMd5Realm realm = new CustomerMd5Realm();
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
credentialsMatcher.setHashAlgorithmName("md5");
credentialsMatcher.setHashIterations(1024);
realm.setCredentialsMatcher(credentialsMatcher);
defaultSecurityManager.setRealm(realm);
SecurityUtils.setSecurityManager(defaultSecurityManager);
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("xiaochen", "123");
try {
subject.login(token);
System.out.println("登录成功");
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户名错误");
}catch (IncorrectCredentialsException e){
e.printStackTrace();
System.out.println("密码错误");
}
if(subject.isAuthenticated()){
System.out.println(subject.hasRole("super"));
System.out.println(subject.hasAllRoles(Arrays.asList("admin", "super")));
boolean[] booleans = subject.hasRoles(Arrays.asList("admin", "super", "user"));
for (boolean aBoolean : booleans) {
System.out.println(aBoolean);
}
System.out.println("==============================================");
System.out.println("权限:"+subject.isPermitted("user:update:01"));
System.out.println("权限:"+subject.isPermitted("product:create:02"));
boolean[] permitted = subject.isPermitted("user:*:01", "order:*:10");
for (boolean b : permitted) {
System.out.println(b);
}
boolean permittedAll = subject.isPermittedAll("user:*:01", "product:create:01");
System.out.println(permittedAll);
}
}
}
6 整合SpringBoot项目实战
6.1 整合思路
6.2 引入shiro依赖
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-spring-boot-starterartifactId>
<version>1.5.3version>
dependency>
6.3 配置shiro环境
6.3.1 创建配置类
配置shiroFilterFactoryBean
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(SecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
return shiroFilterFactoryBean;
}
配置WebSecurityManager
@Bean
public DefaultWebSecurityManager getSecurityManager(Realm realm){
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
defaultWebSecurityManager.setRealm(realm);
return defaultWebSecurityManager;
}
创建自定义realm
public class CustomerRealm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws
AuthenticationException {
return null;
}
}
配置自定义realm
@Bean
public Realm getRealm(){
return new CustomerRealm();
}
编写控制器跳转至index.html
@Controller
public class IndexController {
@RequestMapping("index")
public String index(){
System.out.println("跳转至主页");
return "index";
}
}
启动springboot应用访问index
注意:
默认在配置好shiro环境后默认环境中没有对项目中任何资源进
行权限控制,所有现在项目中所有资源都可以通过路径访问
加入权限控制
修改ShiroFilterFactoryBean配置
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map<String,String> map = new LinkedHashMap<>();
map.put("/index.jsp","authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
/**代表拦截项目中一切资源 ,authc代表shiro中的一个filter的
别名,详细内容看文档的shirofilter列表
注意:shiro认证页面路径为/login.jsp.如果认证没通过,会跳
到这个页面。
6.4 常见过滤器
注意: shiro提供和多个默认的过滤器,我们可以用这些过滤器来配
置控制指定url的权限:
配置缩写 |
对应的过滤器 |
功能 |
anon |
AnonymousFilter |
指定url可以匿名访问 |
authc |
FormAuthenticationFilter |
指定url需要form表单登录,默认会从请求中获取username 、password ,rememberMe 等参数并尝试登录,如果登录不了就会跳转到loginUrl配置的路径。我们也可以用这个过滤器做默认的登录逻辑,但是一般都是我们自己在控制器写登录逻辑的,自己写的话出错返回的信息都可以定制嘛。 |
authcBasic |
BasicHttpAuthenticationFilter |
指定url需要basic登录 |
logout |
LogoutFilter |
登出过滤器,配置指定url就可以实现退出功能,非常方便 |
noSessionCreation |
NoSessionCreationFilter |
禁止创建会话 |
perms |
PermissionsAuthorizationFilter |
需要指定权限才能访问 |
port |
PortFilter |
需要指定端口才能访问 |
rest |
HttpMethodPermissionFilter |
将http请求方法转化成相应的动词来构造一个权限字符串,这个感觉意义不大,有兴趣自己看源码的注释 |
roles |
RolesAuthorizationFilter |
需要指定角色才能访问 |
ssl |
SslFilter |
需要https请求才能访问 |
user |
UserFilter |
需要已登录或“记住我”的用户才能访问 |
6.5 认证实现
在login.jsp中开发认证界面
<form action="${pageContext.request.contextPath}/user/login" method="post">
用户名:<input type="text" name="username" > <br/>
密码 : <input type="text" name="password"> <br>
<input type="submit" value="登录">
form>
开发controller
@Controller
@RequestMapping("user")
public class UserController {
@RequestMapping("login")
public String login(String username,String password){
Subject subject = SecurityUtils.getSubject();
try {
subject.login(new UsernamePasswordToken(username,password));
return "redirect:/index.jsp";
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户名错误!");
}catch (IncorrectCredentialsException e){
e.printStackTrace();
System.out.println("密码错误!");
}
return "redirect:/login.jsp";
}
}
## 注意:当注册WebSecurityManager时候,我们不需要利用
SecurityUtils注册
WebSecurityManager。因为当配置WebSecurityManager的时候。
会自动注册到SecurityUtils
开发realm中返回静态数据(未连接数据库)
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("==========================");
String principal = (String) token.getPrincipal();
if("xiaochen".equals(principal)){
return new SimpleAuthenticationInfo(principal,"123",this.getName());
}
return null;
}
}
启动项目以realm中定义静态数据进行认证
认证功能没有md5和随机盐的认证就实现啦
6.6 退出认证
开发页面退出连接
开发controller
@Controller
@RequestMapping("user")
public class UserController {
@RequestMapping("logout")
public String logout(){
Subject subject = SecurityUtils.getSubject();
subject.logout();
return "redirect:/login.jsp";
}
}
修改退出连接访问退出路径
退出之后访问受限资源立即返回认证界面
6.7 MD5、Salt的认证实现
6.7.1 开发数据库注册
开发注册界面
<h1>用户注册h1>
<form action="${pageContext.request.contextPath}/user/register" method="post">
用户名:<input type="text" name="username" > <br/>
密码 : <input type="text" name="password"> <br>
<input type="submit" value="立即注册">
form>
创建数据表结构
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user` (
`id` int(6) NOT NULL AUTO_INCREMENT,
`username` varchar(40) DEFAULT NULL,
`password` varchar(40) DEFAULT NULL,
`salt` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
SET FOREIGN_KEY_CHECKS = 1;
项目引入依赖
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>2.1.2version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.38version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>1.1.19version>
dependency>
配置application.properties配置文件
server.port=8888
server.servlet.context-path=/shiro
spring.application.name=shiro
spring.mvc.view.prefix=/
spring.mvc.view.suffix=.jsp
#新增配置
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/shiro?characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=root
mybatis.type-aliases-package=com.baizhi.springboot_jsp_shiro.entity
mybatis.mapper-locations=classpath:com/baizhi/mapper/*.xml
创建entity
@Data
@Accessors(chain = true)
@AllArgsConstructor
@NoArgsConstructor
public class User {
private String id;
private String username;
private String password;
private String salt;
}
创建DAO接口
@Mapper
public interface UserDAO {
void save(User user);
}
开发mapper配置文件
<insert id="save" parameterType="User" useGeneratedKeys="true" keyProperty="id">
insert into t_user values(#{id},#{username},#{password},#{salt})
insert>
开发service接口
public interface UserService {
void register(User user);
}
创建salt工具类
public class SaltUtils {
public static String getSalt(int n){
char[] chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567890!@#$%^&*()".toCharArray();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; i++) {
char aChar = chars[new Random().nextInt(chars.length)];
sb.append(aChar);
}
return sb.toString();
}
}
开发service实现类
@Service
@Transactional
public class UserServiceImpl implements UserService {
@Autowired
private UserDAO userDAO;
@Override
public void register(User user) {
String salt = SaltUtils.getSalt(8);
user.setSalt(salt);
Md5Hash md5Hash = new Md5Hash(user.getPassword(),salt,1024);
user.setPassword(md5Hash.toHex());
userDAO.save(user);
}
}
开发Controller
@Controller
@RequestMapping("user")
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("register")
public String register(User user) {
try {
userService.register(user);
return "redirect:/login.jsp";
}catch (Exception e){
e.printStackTrace();
return "redirect:/register.jsp";
}
}
}
启动项目进行注册
6.7.2 开发数据库认证
开发DAO
@Mapper
public interface UserDAO {
void save(User user);
User findByUserName(String username);
}
开发mapper配置文件
<select id="findByUserName" parameterType="String" resultType="User">
select id,username,password,salt from t_user
where username = #{username}
select>
开发Service接口
public interface UserService {
void register(User user);
User findByUserName(String username);
}
开发Service实现类
@Service("userService")
@Transactional
public class UserServiceImpl implements UserService {
@Autowired
private UserDAO userDAO;
@Override
public User findByUserName(String username) {
return userDAO.findByUserName(username);
}
}
开发在工厂中获取bean对象的工具类
@Component
public class ApplicationContextUtils implements ApplicationContextAware {
private static ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.context = applicationContext;
}
public static Object getBean(String beanName){
return context.getBean(beanName);
}
}
修改自定义realm
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("==========================");
String principal = (String) token.getPrincipal();
UserService userService = (UserService) ApplicationContextUtils.getBean("userService");
User user = userService.findByUserName(principal);
if(!ObjectUtils.isEmpty(user)){
return new SimpleAuthenticationInfo(user.getUsername(),user.getPassword(),
ByteSource.Util.bytes(user.getSalt()),this.getName());
}
return null;
}
修改ShiroConfig中realm使用凭证匹配器以及hash散列
@Bean
public Realm getRealm(){
CustomerRealm customerRealm = new CustomerRealm();
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
credentialsMatcher.setHashAlgorithmName("md5");
credentialsMatcher.setHashIterations(1024);
customerRealm.setCredentialsMatcher(credentialsMatcher);
return customerRealm;
}
6.8 授权实现
页面资源授权
<%@taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
<shiro:hasAnyRoles name="user,admin">
<li><a href="">用户管理a>
<ul>
<shiro:hasPermission name="user:add:*">
<li><a href="">添加a>li>
shiro:hasPermission>
<shiro:hasPermission name="user:delete:*">
<li><a href="">删除a>li>
shiro:hasPermission>
<shiro:hasPermission name="user:update:*">
<li><a href="">修改a>li>
shiro:hasPermission>
<shiro:hasPermission name="user:find:*">
<li><a href="">查询a>li>
shiro:hasPermission>
ul>
li>
shiro:hasAnyRoles>
<shiro:hasRole name="admin">
<li><a href="">商品管理a>li>
<li><a href="">订单管理a>li>
<li><a href="">物流管理a>li>
shiro:hasRole>
代码方式授权
@RequestMapping("save")
public String save(){
System.out.println("进入方法");
Subject subject = SecurityUtils.getSubject();
if (subject.hasRole("admin")) {
System.out.println("保存订单!");
}else{
System.out.println("无权访问!");
}
return "redirect:/index.jsp";
}
方法调用授权
@RequiresRoles 用来基于角色进行授权
@RequiresPermissions 用来基于权限进行授权
@RequiresRoles(value={"admin","user"})
@RequiresPermissions("user:update:01")
@RequestMapping("save")
public String save(){
System.out.println("进入方法");
return "redirect:/index.jsp";
}
授权数据持久化
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
DROP TABLE IF EXISTS `t_pers`;
CREATE TABLE `t_pers` (
`id` int(6) NOT NULL AUTO_INCREMENT,
`name` varchar(80) DEFAULT NULL,
`url` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `t_role`;
CREATE TABLE `t_role` (
`id` int(6) NOT NULL AUTO_INCREMENT,
`name` varchar(60) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `t_role_perms`;
CREATE TABLE `t_role_perms` (
`id` int(6) NOT NULL,
`roleid` int(6) DEFAULT NULL,
`permsid` int(6) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user` (
`id` int(6) NOT NULL AUTO_INCREMENT,
`username` varchar(40) DEFAULT NULL,
`password` varchar(40) DEFAULT NULL,
`salt` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `t_user_role`;
CREATE TABLE `t_user_role` (
`id` int(6) NOT NULL,
`userid` int(6) DEFAULT NULL,
`roleid` int(6) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
SET FOREIGN_KEY_CHECKS = 1;
创建dao方法
User findRolesByUserName(String username);
List<Perms> findPermsByRoleId(String id);
mapper实现
<resultMap id="userMap" type="User">
<id column="uid" property="id"/>
<result column="username" property="username"/>
<collection property="roles" javaType="list" ofType="Role">
<id column="id" property="id"/>
<result column="rname" property="name"/>
collection>
resultMap>
<select id="findRolesByUserName" parameterType="String" resultMap="userMap">
SELECT u.id uid,u.username,r.id,r.NAME rname
FROM t_user u
LEFT JOIN t_user_role ur
ON u.id=ur.userid
LEFT JOIN t_role r
ON ur.roleid=r.id
WHERE u.username=#{username}
select>
<select id="findPermsByRoleId" parameterType="String" resultType="Perms">
SELECT p.id,p.NAME,p.url,r.NAME
FROM t_role r
LEFT JOIN t_role_perms rp
ON r.id=rp.roleid
LEFT JOIN t_perms p ON rp.permsid=p.id
WHERE r.id=#{id}
select>
Service接口
User findRolesByUserName(String username);
List<Perms> findPermsByRoleId(String id);
Service实现
@Override
public List<Perms> findPermsByRoleId(String id) {
return userDAO.findPermsByRoleId(id);
}
@Override
public User findRolesByUserName(String username) {
return userDAO.findRolesByUserName(username);
}
修改自定义realm
public class CustomerRealm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String primaryPrincipal = (String) principals.getPrimaryPrincipal();
System.out.println("调用授权验证: "+primaryPrincipal);
UserService userService = (UserService) ApplicationContextUtils.getBean("userService");
User user = userService.findRolesByUserName(primaryPrincipal);
if(!CollectionUtils.isEmpty(user.getRoles())){
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
user.getRoles().forEach(role->{
simpleAuthorizationInfo.addRole(role.getName());
List<Perms> perms = userService.findPermsByRoleId(role.getId());
if(!CollectionUtils.isEmpty(perms)){
perms.forEach(perm->{
simpleAuthorizationInfo.addStringPermission(perm.getName());
});
}
});
return simpleAuthorizationInfo;
}
return null;
}
}
6.9 使用CacheManager
Cache 作用
Cache 缓存: 计算机内存中一段数据
作用: 用来减轻DB的访问压力,从而提高系统的查询效率
流程:
6.9.1 使用shiro中默认EhCache实现缓存
引入依赖
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-ehcacheartifactId>
<version>1.5.3version>
dependency>
开启缓存
@Bean
public Realm getRealm(){
CustomerRealm customerRealm = new CustomerRealm();
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
credentialsMatcher.setHashAlgorithmName("MD5");
credentialsMatcher.setHashIterations(1024);
customerRealm.setCredentialsMatcher(credentialsMatcher);
customerRealm.setCacheManager(new RedisCacheManager());
customerRealm.setCachingEnabled(true);
customerRealm.setAuthenticationCachingEnabled(true);
customerRealm.setAuthenticationCacheName("authenticationCache");
customerRealm.setAuthorizationCachingEnabled(true);
customerRealm.setAuthorizationCacheName("authorizationCache");
return customerRealm;
}
启动刷新页面进行测试
注意:如果控制台没有任何sql展示说明缓存已经开启
6.9.2 shiro中使用Redis作为缓存实现
引入redis依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
配置redis连接
spring.redis.port=6379
spring.redis.host=localhost
spring.redis.database=0
启动redis服务
开发RedisCacheManager
public class RedisCacheManager implements CacheManager {
@Override
public <K, V> Cache<K, V> getCache(String cacheName) throws CacheException {
System.out.println("缓存名称: "+cacheName);
return new RedisCache<K,V>(cacheName);
}
}
开RedisCache实现
public class RedisCache<K,V> implements Cache<K,V> {
private String cacheName;
public RedisCache() {
}
public RedisCache(String cacheName) {
this.cacheName = cacheName;
}
@Override
public V get(K k) throws CacheException {
System.out.println("获取缓存:"+ k);
return (V) getRedisTemplate().opsForHash().get(this.cacheName,k.toString());
}
@Override
public V put(K k, V v) throws CacheException {
System.out.println("设置缓存key: "+k+" value:"+v);
getRedisTemplate().opsForHash().put(this.cacheName,k.toString(),v);
return null;
}
@Override
public V remove(K k) throws CacheException {
return (V) getRedisTemplate().opsForHash().delete(this.cacheName,k.toString());
}
@Override
public v remove(k k) throws CacheException {
return (v) getRedisTemplate().opsForHash().delete(this.cacheName,k.toString());
}
@Override
public void clear() throws CacheException {
getRedisTemplate().delete(this.cacheName);
}
@Override
public int size() {
return getRedisTemplate().opsForHash().size(this.cacheName).intValue();
}
@Override
public Set<k> keys() {
return getRedisTemplate().opsForHash().keys(this.cacheName);
}
@Override
public Collection<v> values() {
return getRedisTemplate().opsForHash().values(this.cacheName);
}
private RedisTemplate getRedisTemplate(){
RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtils.getBean("redisTemplate");
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
return redisTemplate;
}
private RedisTemplate getRedisTemplate(){
RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtils.getBean("redisTemplate");
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
return redisTemplate;
}
}
启动项目测试发现报错
错误解释: 由于shiro中提供的simpleByteSource实现没有实现
序列化,所有在认证时出现错误信息
解决方案: 需要自动salt实现序列化
自定义salt实现序列化
public class MyByteSource extends SimpleByteSource implements Serializable {
public MyByteSource(String string) {
super(string);
}
}
在realm中使用自定义salt
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("==========================");
String principal = (String) token.getPrincipal();
UserService userService = (UserService) ApplicationContextUtils.getBean("userService");
User user = userService.findByUserName(principal);
if(!ObjectUtils.isEmpty(user)){
return new SimpleAuthenticationInfo(user.getUsername(),user.getPassword(),
new MyByteSource(user.getSalt()),this.getName());
}
return null;
}
再次启动测试,发现可以成功放入redis缓存
6.9.3 加入验证码验证
开发页面加入验证码
开发控制器
@RequestMapping("getImage")
public void getImage(HttpSession session, HttpServletResponse response) throws IOException {
String code = VerifyCodeUtils.generateVerifyCode(4);
session.setAttribute("code",code);
ServletOutputStream os = response.getOutputStream();
response.setContentType("image/png");
VerifyCodeUtils.outputImage(220,60,os,code);
}
放行验证码
- 开发页面
修改认证流程
@RequestMapping("login")
public String login(String username, String password,String code,HttpSession session) {
String codes = (String) session.getAttribute("code");
try {
if (codes.equalsIgnoreCase(code)){
Subject subject = SecurityUtils.getSubject();
subject.login(new UsernamePasswordToken(username, password));
return "redirect:/index.jsp";
}else{
throw new RuntimeException("验证码错误!");
}
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户名错误!");
} catch (IncorrectCredentialsException e) {
e.printStackTrace();
System.out.println("密码错误!");
}catch (Exception e){
e.printStackTrace();
System.out.println(e.getMessage());
}
return "redirect:/login.jsp";
}
修改salt不能序列化的问题
public class MyByteSource implements ByteSource,Serializable {
private byte[] bytes;
private String cachedHex;
private String cachedBase64;
public MyByteSource(){
}
public MyByteSource(byte[] bytes) {
this.bytes = bytes;
}
public MyByteSource(char[] chars) {
this.bytes = CodecSupport.toBytes(chars);
}
public MyByteSource(String string) {
this.bytes = CodecSupport.toBytes(string);
}
public MyByteSource(ByteSource source) {
this.bytes = source.getBytes();
}
public MyByteSource(File file) {
this.bytes = (new MyByteSource.BytesHelper()).getBytes(file);
}
public MyByteSource(InputStream stream) {
this.bytes = (new MyByteSource.BytesHelper()).getBytes(stream);
}
public static boolean isCompatible(Object o) {
return o instanceof byte[] || o instanceof char[] || o instanceof String || o instanceof ByteSource || o instanceof File || o instanceof InputStream;
}
public byte[] getBytes() {
return this.bytes;
}
public boolean isEmpty() {
return this.bytes == null || this.bytes.length == 0;
}
public String toHex() {
if (this.cachedHex == null) {
this.cachedHex = Hex.encodeToString(this.getBytes());
}
return this.cachedHex;
}
public String toBase64() {
if (this.cachedBase64 == null) {
this.cachedBase64 = Base64.encodeToString(this.getBytes());
}
return this.cachedBase64;
}
public String toString() {
return this.toBase64();
}
public int hashCode() {
return this.bytes != null && this.bytes.length != 0 ? Arrays.hashCode(this.bytes) : 0;
}
public boolean equals(Object o) {
if (o == this) {
return true;
} else if (o instanceof ByteSource) {
ByteSource bs = (ByteSource)o;
return Arrays.equals(this.getBytes(), bs.getBytes());
} else {
return false;
}
}
private static final class BytesHelper extends CodecSupport {
private BytesHelper() {
}
public byte[] getBytes(File file) {
return this.toBytes(file);
}
public byte[] getBytes(InputStream stream) {
return this.toBytes(stream);
}
}
}
7 Shiro整合springboot之thymeleaf权限控制
7.1 引入扩展依赖
<dependency>
<groupId>com.github.theborakompanionigroupId>
<artifactId>thymeleaf-extras-shiroartifactId>
<version>2.0.0version>
dependency>
7.2 页面中引入命名空间
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
......
7.3 常见权限控制标签使用
<p shiro:guest="">Please <a href="login.html">logina>p>
<p shiro:user="">
Welcome back John! Not John? Click <a href="login.html">herea> to login.
p>
<p shiro:authenticated="">
Hello, <span shiro:principal="">span>, how are you today?
p>
<a shiro:authenticated="" href="updateAccount.html">Update your contact informationa>
<p>Hello, <shiro:principal/>, how are you today?p>
<p shiro:notAuthenticated="">
Please <a href="login.html">logina> in order to update your credit card information.
p>
<a shiro:hasRole="admin" href="admin.html">Administer the systema>
<p shiro:lacksRole="developer">
Sorry, you are not allowed to developer the system.
p>
<p shiro:hasAllRoles="developer, 2">
You are a developer and a admin.
p>
<p shiro:hasAnyRoles="admin, vip, developer,1">
You are a admin, vip, or developer.
p>
<a shiro:hasPermission="userInfo:add" href="createUser.html">添加用户a>
<p shiro:lacksPermission="userInfo:del">
Sorry, you are not allowed to delete user accounts.
p>
<p shiro:hasAllPermissions="userInfo:view, userInfo:add">
You can see or add users.
p>
<p shiro:hasAnyPermissions="userInfo:view, userInfo:del">
You can see or delete users.
p>
<a shiro:hasPermission="pp" href="createUser.html">Create a new Usera>
7.4 加入shiro的方言配置
页面标签不起作用一定要记住加入方言处理
@Bean(name = "shiroDialect")
public ShiroDialect shiroDialect(){
return new ShiroDialect();
}
JWT
1 什么是JWT
JSON Web Token (JWT) is an open standard ([RFC 7519]
(https://tools.ietf.org/html/rfc7519)) that defines a compact
and self-contained way for securely transmitting
information between parties as a JSON object. This
information can be verified and trusted because it is
digitally signed. JWTs can be signed using a secret
(with the HMAC algorithm) or a public/private
key pair using RSA or ECDSA
---[摘自官网]
# 1.翻译
官网地址: https://jwt.io/introduction/
翻译: jsonwebtoken(JWT)是一个开放标准(rfc7519),
它定义了一种紧凑的、自包含的方式,用于在各方之间以JSON
对象安全地传输信息。此信息可以验证和信任,因为它是数字签
名的。jwt可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的
公钥/私钥对进行签名
# 2.通俗解释
JWT简称JSON Web Token,也就是通过JSON形式作为Web应
用中的令牌,用于在各方之间安全地将信息作为JSON对象传输。
在数据传输过程中还可以完成数据加密、签名等相关处理。
2 JWT能做什么
# 1.授权
这是使用JWT的最常见方案。一旦用户登录,每个后续请求将
包括JWT,从而允允许的路由,服务和资源。单点登录是当今
广泛使用JWT的一项功能,因为它的开销很小并且可以在不同
的域中轻松使用。
# 2.信息交换
JSON Web Token是在各方之间安全地传输信息的好方法。因为
可以对JWT进行签名(例如,使用公钥/私钥对),所以您可
以确保发件人是他们所说的人。此外,由于签名是使用标头和有
效负载计算的,因此您还可以验证内容是否遭到篡改。
注意:jwt跟session不一样,jwt存储在客户端,session
存储在服务器端,服务器断电后session就没了,而jwt因
为存储在客户端,所以就不会被影响,只要jwt不过期,
就可以继续使用。
3 为什么是JWT
3.1 基于传统的Session认证
# 1.认证方式
我们知道,http协议本身是一种无状态的协议,而这
就意味着如果用户向我们的应用提供了用户名和密码来进行用
户认证,那么下一次请求时,用户还要再一次进行用户认证
才行,因为根据http协议,我们并不能知道是哪个用户发出
的请求,所以为了让我们的应用能识别是哪个用户发出的请求,我
们只能在服务器存储一份用户登录的信息,这份登录信息会在响
应时传递给浏览器,告诉其保存为cookie,以便下次请求时发送
给我们的应用,这样我们的应用就能识别请求来自哪个用
户了,这就是传统的基于session认证。
t
# 2.认证流程
# 3.暴露问题
1.每个用户经过我们的应用认证之后,我们的应用都要在服务端做
一次记录,以方便用户下次请求的鉴别,通常而言session都是保
存在内存中,而随着认证用户的增多,服务端的开销会明显增大
2.用户认证之后,服务端做认证记录,如果认证的记录被保存在
内存中的话,这意味着用户下次请求还必须要请求在这台服务
器上,这样才能拿到授权的资源,这样在分布式的应用上,相应
的限制了负载均衡器的能力。这也意味着限制了应用的扩展能力。
3.因为是基于cookie来进行用户识别的, cookie如果被截获,用户
就会很容易受到跨站请求伪造的攻击。
4.在前后端分离系统中就更加痛苦:如下图所示
也就是说前后端分离在应用解耦后增加了部署的复杂性。通常用户
一次请求就要转发多次。如果用session 每次携带sessionid 到服务
器,服务器还要查询用户信息。同时如果用户很多。这些信息存储
在服务器内存中,给服务器增加负担。还有就是CSRF(跨站伪造
请求攻 击)攻击,session是基于cookie进行用户识别的, cookie如
果被截获,用户就会很容易受到跨站请求伪造的攻击。还有就是
sessionid就是一个特征值,表达的信息不够丰富。不容易扩展。
而且如果你后端应用是多节点部署。那么就需要实现session共
享机制。不方便集群应用。
3.2 基于JWT认证
# 1.认证流程
首先,前端通过Web表单将自己的用户名和密码发送到后端的接口。
这一过程一般是一个HTTP POST请求。建议的方式是通过SSL加
密的传输(https协议),从而避免敏感信息被嗅探。
后端核对用户名和密码成功后,将用户的id等其他信息作为
JWT Payload(负载),将其与头部分别进行Base64编码拼接后
签名,形成一个JWT(Token)。形成的JWT就是一个形同lll.zzz.xxx
的字符串。 token head.payload.singurater
后端将JWT字符串作为登录成功的返回结果返回给前端。前端可
以将返回的结果保存在localStorage或sessionStorage上,退出登录
时前端删除保存的JWT即可。
前端在每次请求时将JWT放入HTTP Header中的Authorization位。
(解决XSS和XSRF问题) HEADER
后端检查是否存在,如存在验证JWT的有效性。例如,检查签名
是否正确;检查Token是否过期;检查Token的接收方是否是自己(可选)。
验证通过后后端使用JWT中包含的用户信息进行其他逻辑操作,
返回相应结果。
# 2.jwt优势
简洁(Compact): 可以通过URL,POST参数或者在HTTP header发送,
因为数据量小,传输速度也很快
自包含(Self-contained):负载中包含了所有用户所需要的信息,
避免了多次查询数据库
因为Token是以JSON加密的形式保存在客户端的,所以JWT是跨语
言的,原则上任何web形式都支持。
不需要在服务端保存会话信息,特别适用于分布式微服务。
4 JWT的结构是什么
token string ====> header.payload.singnature token
# 1.令牌组成
- 1.标头(Header)
- 2.有效载荷(Payload)
- 3.签名(Signature)
- 因此,JWT通常如下所示:xxxxx.yyyyy.zzzzz Header.Payload.Signature
# 2.Header
- 标头通常由两部分组成:令牌的类型(即JWT)和所使用的签名算法,例如HMAC SHA256或RSA。它会使用 Base64 编码组成 JWT 结构的第一部分。
- 注意:Base64是一种编码,也就是说,它是可以被翻译回原来的样子来的。它并不是一种加密过程。
{
"alg": "HS256",
"typ": "JWT"
}
# 3.Payload
- 令牌的第二部分是有效负载,其中包含声明。声明是有关实体
- (通常是用户)和其他数据的声明。同样的,它会使用
Base64 编码组成 JWT 结构的第二部分
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
# 4.Signature
- 前面两部分都是使用 Base64 进行编码的,即前端可以解开知
道里面的信息。Signature 需要使用编码后的 header 和 payload
以及我们提供的一个密钥,然后使用 header 中指定的签名算法
(HS256)进行签名。签名的作用是保证 JWT 没有被篡改过
- 如:
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload),secret);
# 签名目的
- 最后一步签名的过程,实际上是对头部以及负载内容进行签名,
防止内容被窜改。如果有人对头部以及负载的内容解码之后进行修
改,再进行编码,最后加上之前的签名组合形成新的JWT的话,
那么服务器端会判断出新的头部和负载形成的签名和JWT附带上
的签名是不一样的。如果要对新的头部和负载进行签名,在不知
道服务器加密时用的密钥的话,得出来的签名也是不一样的。
# 信息安全问题
- 在这里大家一定会问一个问题:Base64是一种编码,
是可逆的,那么我的信息不就被暴露了吗?
- 是的。所以,在JWT中,不应该在负载里面加入任何敏感的
数据。在上面的例子中,我们传输的是用户的User ID。这个值
实际上不是什么敏 感内容,一般情况下被知道也是安全的。但
是像密码这样的内容就不能被放在JWT中了。如果将用户的密
码放在了JWT中,那么怀有恶意的第 三方通过Base64解码就能
很快地知道你的密码了。因此JWT适合用于向Web应用传递一些
非敏感信息。JWT还经常用于设计用户认证和授权系 统,甚至
实现Web应用的单点登录。
# 5.放在一起
- 输出是三个由点分隔的Base64-URL字符串,可以在HTML和
HTTP环境中轻松传递这些字符串,与基于XML的标准(例如SAML)
相比,它更紧凑。
- 简洁(Compact)
可以通过URL, POST 参数或者在 HTTP header 发送,因为
数据量小,传输速度快
- 自包含(Self-contained)
负载中包含了所有用户所需要的信息,避免了多次查询数据库
5 使用JWT
# 1.引入依赖
<dependency>
<groupId>com.auth0groupId>
<artifactId>java-jwtartifactId>
<version>3.4.0version>
dependency>
# 2.生成token
Calendar instance = Calendar.getInstance();
instance.add(Calendar.SECOND, 90);
String token = JWT.create()
.withClaim("username", "张三")
.withExpiresAt(instance.getTime())
.sign(Algorithm.HMAC256("token!Q2W#E$RW"));
System.out.println(token);
生成结果
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOlsicGhvbmUiLCIxNDMyMzIzNDEzNCJdLCJleHAiOjE1OTU3Mzk0NDIsInVzZXJuYW1lIjoi5byg5LiJIn0.aHmE3RNqvAjFr_dvyn_sD2VJ46P7EGiS5OBMO_TI5jg
# 3.根据令牌和签名解析数据
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("token!Q2W#E$RW")).build();
DecodedJWT decodedJWT = jwtVerifier.verify(token);
System.out.println("用户名: " + decodedJWT.getClaim("username").asString()); 、
System.out.println("过期时间: "+decodedJWT.getExpiresAt());
# 4.常见异常信息
SignatureVerificationException: 签名不一致异常
TokenExpiredException: 令牌过期异常
AlgorithmMismatchException: 算法不匹配异常
InvalidClaimException: 失效的payload异常
6 封装工具类
public class JWTUtils {
private static String TOKEN = "token!Q@W3e4r";
public static String getToken(Map<String,String> map){
JWTCreator.Builder builder = JWT.create();
map.forEach((k,v)->{
builder.withClaim(k,v);
});
Calendar instance = Calendar.getInstance();
instance.add(Calendar.SECOND,7);
builder.withExpiresAt(instance.getTime());
return builder.sign(Algorithm.HMAC256(TOKEN));
}
public static void verify(String token){
JWT.require(Algorithm.HMAC256(TOKEN)).build().verify(token);
}
public static DecodedJWT getToken(String token){
return JWT.require(Algorithm.HMAC256(TOKEN)).build().verify(token);
}
}
7 整合springboot
# 0.搭建springboot+mybatis+jwt环境
- 引入依赖
- 编写配置
<dependency>
<groupId>com.auth0groupId>
<artifactId>java-jwtartifactId>
<version>3.4.0version>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>2.1.3version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.18.12version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>1.1.19version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.38version>
dependency>
server.port=8989
spring.application.name=jwt
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/jwt?characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=root
mybatis.type-aliases-package=com.baizhi.entity
mybatis.mapper-locations=classpath:com/baizhi/mapper/*.xml
logging.level.com.baizhi.dao=debug
1. 开发数据库
- 这里采用最简单的表结构验证JWT使用
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` varchar(80) DEFAULT NULL COMMENT '用户名',
`password` varchar(40) DEFAULT NULL COMMENT '用户密码',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
2. 开发entity
@Data
@Accessors(chain=true)
public class User {
private String id;
private String name;
private String password;
}
3.开发DAO接口和mapper.xml
@Mapper
public interface UserDAO {
User login(User user);
}
<mapper namespace="com.baizhi.dao.UserDAO">
<select id="login" parameterType="User" resultType="User">
select * from user where name=#{name} and password = #{password}
select>
mapper>
4.开发Service 接口以及实现类
public interface UserService {
User login(User user);
}
@Service
@Transactional
public class UserServiceImpl implements UserService {
@Autowired
private UserDAO userDAO;
@Override
@Transactional(propagation = Propagation.SUPPORTS)
public User login(User user) {
User userDB = userDAO.login(user);
if(userDB!=null){
return userDB;
}
throw new RuntimeException("登录失败~~");
}
}
5.开发controller
@RestController
@Slf4j
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/user/login")
public Map<String,Object> login(User user) {
Map<String,Object> result = new HashMap<>();
log.info("用户名: [{}]", user.getName());
log.info("密码: [{}]", user.getPassword());
try {
User userDB = userService.login(user);
Map<String, String> map = new HashMap<>();
map.put("id",userDB.getId());
map.put("username", userDB.getName());
String token = JWTUtils.getToken(map);
result.put("state",true);
result.put("msg","登录成功!!!");
result.put("token",token);
} catch (Exception e) {
e.printStackTrace();
result.put("state","false");
result.put("msg",e.getMessage());
}
return result;
}
}
6.数据库添加测试数据启动项目
7.通过postman模拟登录失败
8.通过postman模拟登录成功
9.编写测试接口
@PostMapping("/test/test")
public Map<String, Object> test(String token) {
Map<String, Object> map = new HashMap<>();
try {
JWTUtils.verify(token);
map.put("msg", "验证通过~~~");
map.put("state", true);
} catch (TokenExpiredException e) {
map.put("state", false);
map.put("msg", "Token已经过期!!!");
} catch (SignatureVerificationException e){
map.put("state", false);
map.put("msg", "签名错误!!!");
} catch (AlgorithmMismatchException e){
map.put("state", false);
map.put("msg", "加密算法不匹配!!!");
} catch (Exception e) {
e.printStackTrace();
map.put("state", false);
map.put("msg", "无效token~~");
}
return map;
}
10.通过postman请求接口
11.问题?
- 使用上述方式每次都要传递token数据,每个方法都需要
验证token代码冗余,不够灵活? 如何优化
- 使用拦截器进行优化
public class JWTInterceptor implements HandlerInterceptor{
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("token");
Map<String,Object> map = new HashMap<>();
try {
JWTUtils.verify(token);
return true;
} catch (TokenExpiredException e) {
map.put("state", false);
map.put("msg", "Token已经过期!!!");
} catch (SignatureVerificationException e){
map.put("state", false);
map.put("msg", "签名错误!!!");
} catch (AlgorithmMismatchException e){
map.put("state", false);
map.put("msg", "加密算法不匹配!!!");
} catch (Exception e) {
e.printStackTrace();
map.put("state", false);
map.put("msg", "无效token~~");
}
String json = new ObjectMapper().writeValueAsString(map);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(json);
return false;
}
}
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new JwtTokenInterceptor()).
excludePathPatterns("/user/**")
.addPathPatterns("/**");
}
}
想要获取该该课程markdown笔记(脑图+笔记)。可以扫描以下
微信公众号二维码。或者搜索微信公众号-Java大世界。回复shjwt
即可获取笔记获取方式。