JWT简单说明:
JWT由单短信息构成:
header(base64加密得到第一部分)、playload(base64加密得到第二部分)、signature(将前两部分加密的字符串用 . 号拼接u 拼接,再yong hs256加秘加盐,然后再base64加密,得到第三部分);最后将三部分用 .号连接。
header:由令牌的类型(即JWT)和正在使用过的签名算法组成。如:
{
"alg": "HS256",
"typ": "JWT"
}
playload:
预定义载荷
{
"sub": "1",
"iss": "http://localhost:8000/auth/login",
"iat": 1451888119,
"exp": 1454516119,
"nbf": 1451888119,
"jti": "37c107e4609ddbcc9c096ea5ee76c667",
"aud": "dev"
}
这里面的前 7 个字段都是由官方所定义的,也就是
预定义
(Registered claims)的,并不都是必需的。
iss (issuer):签发人
sub (subject):主题
aud (audience):受众
exp (expiration time):过期时间
nbf (Not Before):生效时间,在此之前是无效的
iat (Issued At):签发时间
jti (JWT ID):编号
公有的载荷
在使用 JWT 时可以额外定义的载荷。为了避免冲突,应该使用
IANA JSON Web Token Registry
中定义好的,或者给额外载荷加上类似命名空间的唯一标识。
私有载荷
在信息交互的双方之间约定好的,既不是预定义载荷也不是公有载荷的一类载荷。
**验证流程:
用户验证成功后会返回给浏览器客户端一个token字符串,服务端不做保存,下次再请求的时候,会携带token;
超时验证-》token合法性验证(前两部分加密对比)-〉签名验证
token只保存在前端,后端只负责校验,不可一修改token,只要一改就认证失败,token签发后,不能手动使其失效,
如果服务器重启,token将失效,需要重新登录。
vue的代码,设置请求头携带toke传递给后台;
//设置请求拦截器,把token传递到后台
axios.interceptors.request.use(function(config){
// console.log(config.url)
//打开进度条
// NProgress.start()
config.headers.Authorization=window.sessionStorage.getItem('token');
return config;
},function(err){
})
springboot后台代码:
pom.xml文件中导入依赖:
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.2.0</version>
</dependency>
创建签名和验证签名的工具类:
@Data
//@TableName实体类的类名和数据库表名不一致
//@TableId实体类的主键名称和表中主键名称不一致,主键策略type
//@TableField实体类中的成员名称和表中字段名称不一致,value是数据库字段名,fill是自动插入、更新等
@TableName("User")
public class User {
//主键策略
@TableId(type=IdType.AUTO)
private Long id;
private String name;
private Integer age;
private String token;
@TableField(value="create_time",fill = FieldFill.INSERT)
private Date createTime;
@TableField(value="update_time",fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
}
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.example.entity.User;
public class TokenUtil {
private static final long EXPIRE_TIME= 15*60*1000;
private static final String TOKEN_SECRET="token123"; //密钥盐
/**
* 签名生成
* @param user
* @return
*/
public static String sign(User user){
String token = null;
try {
Date expiresAt = new Date(System.currentTimeMillis() + EXPIRE_TIME);
token = JWT.create()
.withIssuer("auth0")
.withClaim("username", user.getName())
.withExpiresAt(expiresAt)
// 使用了HMAC256加密算法。
.sign(Algorithm.HMAC256(TOKEN_SECRET));
} catch (Exception e){
e.printStackTrace();
}
return token;
}
/**
* 签名验证
* @param token
* @return
*/
public static boolean verify(String token){
try {
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(TOKEN_SECRET)).withIssuer("auth0").build();
DecodedJWT jwt = verifier.verify(token);
System.out.println("认证通过:");
System.out.println("issuer: " + jwt.getIssuer());
System.out.println("username: " + jwt.getClaim("username").asString());
System.out.println("过期时间: " + jwt.getExpiresAt());
return true;
} catch (Exception e){
return false;
}
}
}
拦截器验证token:
import java.io.PrintWriter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import com.example.utils.TokenUtil;
import net.sf.json.JSONObject;
//拦截器
@Component
public class TokenInterceptor implements HandlerInterceptor{
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,Object handler)throws Exception{
if(request.getMethod().equals("OPTIONS")){
response.setStatus(HttpServletResponse.SC_OK);
return true;
}
response.setCharacterEncoding("utf-8");
String token = request.getHeader("Authorization");
System.out.println("token是"+token);
if(token != null){
boolean result = TokenUtil.verify(token);//验证token
if(result){
System.out.println("通过拦截器");
return true;
}
}
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
PrintWriter out = null;
try{
JSONObject json = new JSONObject();
json.put("success","false");
json.put("msg","认证失败,未通过拦截器");
json.put("code","50000");
response.getWriter().append(json.toString());
System.out.println("认证失败,未通过拦截器");
response.getWriter().write("50000");
}catch (Exception e){
e.printStackTrace();
response.sendError(500);
return false;
}
return false;
}
}
拦截器配置类:
import java.util.ArrayList;
import java.util.List;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import com.example.handler.TokenInterceptor;
/**
* 拦截器配置
*/
@Configuration
public class IntercepterConfig implements WebMvcConfigurer{
private TokenInterceptor tokenInterceptor;
//构造方法
public IntercepterConfig(TokenInterceptor tokenInterceptor){
this.tokenInterceptor = tokenInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry){
List<String> excludePath = new ArrayList<>();
excludePath.add("/user_register"); //注册
excludePath.add("/login"); //登录
excludePath.add("/logout"); //登出
excludePath.add("/static/**"); //静态资源
excludePath.add("/assets/**"); //静态资源
registry.addInterceptor(tokenInterceptor)
.addPathPatterns("/**")
.excludePathPatterns(excludePath);
WebMvcConfigurer.super.addInterceptors(registry);
}
}
登录成功后创建token传递给前端:
@RequestMapping("/login")
// @ResponseBody
public R login(@RequestBody Map<String, String> person) {
User user=new User();
if(person.get("username").equals("lambo")) {
user.setAge(18);
user.setName("lmabo");
user.setId(11L);
//登录成功后创建token传递给客户端
String token = TokenUtil.sign(user);
user.setToken(token);
System.out.println("username is:"+person.get("username"));
return R.ok().data("myinfo",user);
}else {
System.out.println("错误is:"+person.get("username"));
return R.error();
}
}
****shiro
https://www.w3cschool.cn/shiro/co4m1if2.html
http://shiro.apache.org/10-minute-tutorial.html
提供了认证、授权、加密、会话管理,与spring Security 一样都是做一个权限的安全框架;
记住一点,Shiro
不会去维护用户、维护权限;这些需要我们自己去设计/
提供;然后通过相应的接口注入给Shiro
即可。
认证简单的说,就是登录的时候判断你的用户名和密码是否完全匹配,就是证明你是你。
授权,是在认证的基础之上,进行角色和权限的授予。权限决定了一个用户可以进行怎样的操作。
1. Subject:主体,一般指用户。
2. SecurityManager:安全管理器,管理所有Subject,可以配合内部安全组件。(类似于SpringMVC中的DispatcherServlet)
3. Realms:用于进行权限信息的验证,一般需要自己实现。
详细功能:
1. Authentication:身份认证/登录(账号密码验证)。
2. Authorization:授权,即角色或者权限验证。
3. Session Manager:会话管理,用户登录后的session相关管理。
4. Cryptography:加密,密码加密等。
5. Web Support:Web支持,集成Web环境。
6. Caching:缓存,用户信息、角色、权限等缓存到如redis等缓存中。
7. Concurrency:多线程并发验证,在一个线程中开启另一个线程,可以把权限自动传播过去。
8. Testing:测试支持;
9. Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问。
10. Remember Me:记住我,登录后,下次再来的话不用登录了。
使用步骤:
1.导入jar包:
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.1</version>
</dependency>
2.shiro配置类,当中配置拦截器,决定哪些url需要验证,哪些角色需要验证
import java.util.HashMap;
import java.util.Map;
import javax.servlet.Filter;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.spring.web.config.DefaultShiroFilterChainDefinition;
import org.apache.shiro.spring.web.config.ShiroFilterChainDefinition;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ShiroConfig {
//将自己的验证方式加入容器
@Bean
public CustomRealm myShiroRealm() {
CustomRealm customRealm = new CustomRealm();
return customRealm;
}
//权限管理,配置主要是Realm的管理认证
@Bean
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myShiroRealm());
return securityManager;
}
//Filter工厂,设置对应的过滤条件和跳转条件,拦截需要安全控制的URL,然后进行相应的控制,其中authc指定需要认证的uri,anon指定排除认证的uri
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map<String, String> map = new HashMap<>();
//登出
map.put("/logout", "logout");
//对所有用户认证
map.put("/**", "authc");
//不认证的url
map.put("/login", "anon");
3.自定义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.authc.credential.CredentialsMatcher;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import com.example.entity.User;
public class CustomRealm extends AuthorizingRealm{
//数据库存储的用户密码的加密salt,正式环境不能放在源代码里
private static final String encryptSalt = "jwt";
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// TODO Auto-generated method stub
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//加这一步的目的是在Post请求的时候会先进认证,然后在到请求
if (token.getPrincipal() == null) {
return null;
}
//获取用户信息,token中的sername
String username = token.getPrincipal().toString();
//根据token中username到数据库中查询加密过的密码
String encrypassword="";
//获取数据库中加密的盐
User user = new User();
if (user == null) {
//这里返回后会报出对应异常
return null;
} else {
//这里验证authenticationToken和simpleAuthenticationInfo的信息
//第一个参数:token中的username,第二个参数:数据库中加密的密码,第三个参数:加密的盐,第四个参数:reaml对象,
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(username, encrypassword, ByteSource.Util.bytes(encryptSalt), getName());
return simpleAuthenticationInfo;
}
}
/**
* 设置自定义认证加密方式
*
* @param credentialsMatcher 默认加密方式
*/
@Override
public void setCredentialsMatcher(CredentialsMatcher credentialsMatcher) {
//自定义认证加密方式
CustomCredentialsMatcher customCredentialsMatcher = new CustomCredentialsMatcher();
// 设置自定义认证加密方式
super.setCredentialsMatcher(customCredentialsMatcher);
}
}
4.开始配置凭证匹配器,可以用shiro.ini文件,也可以自定义一个匹配器类;
数据库中的密码是加密加盐后的密码,所以我们给realm配置了加密算法的规则,让它将我们传过去的密码进行了同样的加密加盐(这里盐不需要我们设置,是从数据库中查询出来的),然后再和数据库的数据进行比对认证.
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.SimpleCredentialsMatcher;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.util.ByteSource;
/**
* 自定义认证加密方式,这里的加密方式要和注册时密码的加密方式一致
*/
public class CustomCredentialsMatcher extends SimpleCredentialsMatcher {
@Override
public boolean doCredentialsMatch(AuthenticationToken authcToken, AuthenticationInfo info) {
UsernamePasswordToken token = (UsernamePasswordToken) authcToken;
// 获得用户输入的密码:(可以采用加盐(salt)的方式去检验)
String inPassword = new String(((UsernamePasswordToken) authcToken).getPassword());
String username = ((UsernamePasswordToken) authcToken).getUsername();
//获得数据库中的密码
String dbPassword = (String) info.getCredentials();
SimpleAuthenticationInfo saInfo = (SimpleAuthenticationInfo)info;
// ByteSource salt = saInfo.getCredentialsSalt();
ByteSource salt = ByteSource.Util.bytes(username);
//加密类型,密码,盐值,加密次数
Object tokenCredentials = new SimpleHash("md5", inPassword, username, 2).toHex();
//数据库存储密码
Object accountCredentials = getCredentials(info);
//将密码加密与系统加密后的密码校验,内容一致就返回true,不一致就返回false
return equals(tokenCredentials, accountCredentials);
}
}
5.在登录的控制器中:
//带报错的数据
@RequiresRoles("client")
@RequestMapping("/login")
// @ResponseBody
public R login(@RequestBody Map<String, String> person) {
Subject subject = SecurityUtils.getSubject();
try {
//将用户请求参数封装后,直接提交给Shiro处理
UsernamePasswordToken token = new UsernamePasswordToken(person.get("username"), person.get("password"));
subject.login(token);
System.out.println("认证成功");
} catch (UnknownAccountException uae) {
System.out.println("账户不存在");
} catch (IncorrectCredentialsException ice) {
System.out.println("密码不正确");
} catch (LockedAccountException lae) {
System.out.println("用户被锁定了 ");
} catch (AuthenticationException ae) {
//无法判断是什么错了
System.out.println("未知错误");
}
return R.ok().data("myinfo",1);
}