JWT簡單說明:
JWT由單短信息構成:
header(base64加密得到第一部分)、playload(base64加密得到第二部分)、signature(將前兩部分加密的字符串用 . 號拼接u 拼接,再yong hs256加祕加鹽,然後再base64加密,得到第三部分);最後將三部分用 .號連接。
**驗證流程:
用戶驗證成功後會返回給瀏覽器客戶端一個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);
}