shiro+jwt登陸驗證實現(xiàn)教程
學(xué)習(xí)目標(biāo)
例如:
- 一小時實現(xiàn)shiro+jwt實現(xiàn)登錄認(rèn)證
學(xué)習(xí)內(nèi)容
例如:
- 了解shiro框架
- 使用jwt生成身份令牌
項目目錄:

直接上代碼
這個過濾器是我們的重點,這里我們繼承的是Shiro內(nèi)置的BasicHttpAuthenticationFilter ,一個可以內(nèi)置了可以自動登錄方法的的過濾器,有些同學(xué)繼承AuthenticatingFilter也是可以的,根據(jù)個人情況。
@Component
@Slf4j
public class JWTFilter extends BasicHttpAuthenticationFilter {
/**
* 認(rèn)證之前執(zhí)行該方法
* @param request
* @param response
* @param mappedValue
* @return
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
Subject subject = SecurityUtils.getSubject();
return null != subject && subject.isAuthenticated();
}
/**
* 認(rèn)證未通過執(zhí)行該方法
* @param request
* @param response
* @return
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response){
//完成token登入
//1.檢查請求頭中是否含有token
HttpServletRequest httpServletRequest= (HttpServletRequest) request;
String token = httpServletRequest.getHeader("Authorization");
//2. 如果客戶端沒有攜帶token,攔下請求
if(null==token||"".equals(token)){
responseTokenError(response, RCode.INVALID_TOKEN);
return false;
}
//3. 如果有,對進(jìn)行進(jìn)行token驗證
JWTToken jwtToken = new JWTToken(token);
try {
SecurityUtils.getSubject().login(jwtToken);
} catch (AuthenticationException e) {
log.error(e.getMessage());
responseTokenError(response,RCode.AUTH_ERROR);
return false;
}
return true;
}
/**
* 對跨域提供支持
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
// 跨域時會首先發(fā)送一個option請求,這里我們給option請求直接返回正常狀態(tài)
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
/**
* 無需轉(zhuǎn)發(fā),直接返回Response信息 Token認(rèn)證錯誤
*/
private void responseTokenError(ServletResponse response,RCode rCode) {
HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
httpServletResponse.setStatus(HttpStatus.OK.value());
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("application/json; charset=utf-8");
try (PrintWriter out = httpServletResponse.getWriter()) {
String data = new Gson().toJson(R.error(rCode));
out.append(data);
} catch (IOException e) {
e.printStackTrace();
log.error(e.getMessage());
}
}
}
整合shiro的配置,shiro可以幫我們做很多事情,比如創(chuàng)建realm、配置shiro過濾器工廠等。
@Configuration
public class ShiroConfig {
@Bean("securityManager")
public DefaultWebSecurityManager getManager(UserRealm realm) {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
// 使用自己的realm
manager.setRealm(realm);
/*
* 關(guān)閉shiro自帶的session,詳情見文檔
* http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29
*/
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
manager.setSubjectDAO(subjectDAO);
return manager;
}
@Bean("shiroFilter")
public ShiroFilterFactoryBean factory(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
// 添加自己的過濾器并且取名為jwt
Map<String, Filter> filterMap = new HashMap<>();
filterMap.put("jwt", new JWTFilter());
factoryBean.setFilters(filterMap);
factoryBean.setSecurityManager(securityManager);
/*
* 自定義url規(guī)則
* http://shiro.apache.org/web.html#urls-
*/
Map<String, String> filterRuleMap = new HashMap<>();
// 所有請求通過我們自己的JWT Filter
filterRuleMap.put("/**", "jwt");
// 訪問401和404頁面不通過我們的Filter
filterRuleMap.put("/admin/sys/user/login", "anon");
filterRuleMap.put("/user/imgCode", "anon");
//開放API文檔接口
filterRuleMap.put("/swagger-ui.html", "anon");
filterRuleMap.put("/webjars/**","anon");
filterRuleMap.put("/swagger-resources/**","anon");
filterRuleMap.put("/v2/**","anon");
//sql監(jiān)控
filterRuleMap.put("/druid/**","anon");
factoryBean.setFilterChainDefinitionMap(filterRuleMap);
return factoryBean;
}
/**
* 下面的代碼是添加注解支持
*/
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
// 強(qiáng)制使用cglib,防止重復(fù)代理和可能引起代理出錯的問題
// https://zhuanlan.zhihu.com/p/29161098
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
自定義realm,因為我沒有連接數(shù)據(jù)庫這里的用戶名和密碼都是寫死的,根據(jù)業(yè)務(wù)需要可以連接數(shù)據(jù)庫獲取
@Service
public class UserRealm extends AuthorizingRealm {
/**
* 大坑!,必須重寫此方法,不然Shiro會報錯
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JWTToken;
}
/**
* 只有當(dāng)需要檢測用戶權(quán)限的時候才會調(diào)用此方法,例如checkRole,checkPermission之類的
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
/**
* 默認(rèn)使用此方法進(jìn)行用戶名正確與否驗證,錯誤拋出異常即可。
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
String token = (String) auth.getCredentials();
if(JWTUtils.isExpire(token)){
throw new AuthenticationException("token過期,請重新登入!");
}
// 解密獲得username,用于和數(shù)據(jù)庫進(jìn)行對比
String username = JWTUtils.getUsername(token);
if (username == null) {
throw new AuthenticationException("token錯誤,請重新登入!");
}
String name="admin";
String pwd="3bcbb857c763d1429a24959cb8de2593";
if (! JWTUtils.verify(token, name, pwd)) {
throw new AuthenticationException("密碼錯誤!");
}
return new SimpleAuthenticationInfo("activeUser", token, getName());
}
}
public class JWTToken implements AuthenticationToken {
// 密鑰
private String token;
public JWTToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
jwt生成token
public class JWTUtils {
/**
* 過期時間6小時
*/
private static final long EXPIRE_TIME = 6 * 60 * 60 * 1000;
/**
* 校驗token是否正確
*
* @param token 密鑰
* @param secret 用戶的密碼
* @return 是否正確
*/
public static boolean verify(String token, String username, String secret) {
try {
Algorithm algorithm = Algorithm.HMAC256(secret);
JWTVerifier verifier = JWT.require(algorithm)
.withClaim("username", username)
.build();
DecodedJWT jwt = verifier.verify(token);
return true;
} catch (Exception exception) {
return false;
}
}
/**
* 獲得token中的信息無需secret解密也能獲得
*
* @return token中包含的用戶名
*/
public static String getUsername(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("username").asString();
} catch (JWTDecodeException e) {
return null;
}
}
/**
* 生成簽名,2min后過期
*
* @param username 用戶名
* @param secret 用戶的密碼
* @return 加密的token
*/
public static String sign(String username, String secret) {
try {
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
Algorithm algorithm = Algorithm.HMAC256(secret);
// 附帶username信息
return JWT.create()
.withClaim("username", username)
.withExpiresAt(date)
.sign(algorithm);
} catch (Exception e) {
return null;
}
}
/**
* 判斷過期
*
* @param token
* @return
*/
public static boolean isExpire(String token) {
DecodedJWT jwt = JWT.decode(token);
return System.currentTimeMillis() > jwt.getExpiresAt().getTime();
}
}
使用md5進(jìn)行加密
/**
* 密碼加密
* @return
*/
public static String md5Encryption(String source,String salt){
String algorithmName = "MD5";//加密算法
int hashIterations = 1024;//加密次數(shù)
SimpleHash simpleHash = new SimpleHash(algorithmName,source,salt,hashIterations);
return simpleHash+"";
}
重載類:用于結(jié)果返回
@Data
@AllArgsConstructor
@NoArgsConstructor
public class R {
private Integer code ;
private String msg;
private Object data;
public static R ok(Object data){
return new R(RCode.SUCCESS.getCode(),
RCode.SUCCESS.getMsg(),data);
}
public static R ok(){
return R.ok(null);
}
public static R error(RCode rCode){
return new R(rCode.getCode(),rCode.getMsg(),null);
}
public static R error(String message){
return new R(RCode.SYS_ERROR.getCode(),message,null);
}
public static R error(){
return R.error(RCode.ERROR);
}
}
枚舉類
@AllArgsConstructor
@Getter
public enum RCode {
SUCCESS(0,"操作成功"),
ERROR(1,"操作失敗"),
SYS_ERROR(500,"系統(tǒng)錯誤,請與管理員聯(lián)系"),
SYS_VALID_ERROR(501,"參數(shù)校驗錯誤"),
USER_EXIST(1001,"用戶已存在"),
USER_NOT_EXIST(1002,"用戶不存在!"),
VALID_ERROR(40001,"驗證錯誤!"),
INVALID_TOKEN(40410,"Token無效,您無權(quán)訪問該接口"),
AUTH_ERROR(40411,"用戶名或密碼不存在!"),
NO_AUTH_ERROR(40412,"無權(quán)訪問!"),
QUERY_EXIST(1002,"此數(shù)據(jù)以重復(fù)")
;
private Integer code;
private String msg;
}
到這里就可以開始寫controller了,寫了一個登陸的接口和兩個測試的接口
@RestController
@CrossOrigin
@RequestMapping("/admin/sys/user")
public class TestController {
@GetMapping("/login")
public R login(String username,String password){
String MD5Password = MD5Utils.md5Encryption(username, password);
System.out.println(MD5Password);
String token= JWTUtils.sign(username, MD5Password);
System.out.println(token);
JWTToken jwtToken = new JWTToken(token);
try {
SecurityUtils.getSubject().login(jwtToken);
} catch (AuthenticationException e) {
return R.error("token 錯誤!");
}
Map<String,Object> maps = new HashMap<>();
maps.put("token",token);
return R.ok(maps);
}
@GetMapping("all")
public String all(){
return "all";
}
@GetMapping("list")
public R list(){
return R.ok("list");
}
}
測試結(jié)果
如果我不攜帶token是不能訪問任何接口的,除登陸接口外

下面訪問登陸接口,是可以訪問的,并且返回的token令牌,我們可以拿著這個token去訪問其他接口

帶著token就可以正常訪問了,有效期是6個小時

總結(jié)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
win10操作系統(tǒng)下重啟電腦java環(huán)境變量失效
這篇文章主要介紹了win10操作系統(tǒng)下重啟電腦java環(huán)境變量失效,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-09-09
IntelliJ IDEA 中配置 Spring MVC 環(huán)境的詳細(xì)步
這篇文章主要介紹了IntelliJ IDEA 中配置 Spring MVC 環(huán)境的詳細(xì)步驟及問題解決,本文分步驟結(jié)合實例給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友參考下吧2025-04-04
SpringMVC中的@RequestMapping注解解析
這篇文章主要介紹了SpringMVC中的@RequestMapping注解解析,SpringMVC使用@RequestMapping注解為控制器指定可以處理哪些?URL?請求,在控制器的類定義及方法定義處都可標(biāo)注@RequestMapping,需要的朋友可以參考下2023-12-12
SpringMVC源碼解讀之HandlerMapping - AbstractUrlHandlerMapping系列re
這篇文章主要介紹了SpringMVC源碼解讀之HandlerMapping - AbstractUrlHandlerMapping系列request分發(fā) 的相關(guān)資料,需要的朋友可以參考下2016-02-02
Spring多線程通過@Scheduled實現(xiàn)定時任務(wù)
這篇文章主要介紹了Spring多線程通過@Scheduled實現(xiàn)定時任務(wù),@Scheduled?定時任務(wù)調(diào)度注解,是spring定時任務(wù)中最重要的,下文關(guān)于其具體介紹,需要的小伙伴可以參考一下2022-05-05
Java中的ThreadLocal與ThreadLocalMap詳解
這篇文章主要介紹了Java中的ThreadLocal與ThreadLocalMap詳解,ThreadLocal 是一個線程局部變量,其實的功用非常簡單,就是為每一個使用該變量的線程都提供一個變量值的副本,是Java中一種較為特殊的線程綁定機(jī)制,需要的朋友可以參考下2023-09-09

