springboot shiro自定義攔截器結合token實現登入狀態和許可權認證
阿新 • • 發佈:2022-04-14
一、先了解攔截器在http請求中所佔的位置
推薦部落格https://www.freesion.com/article/6875405887/
shiro配置檔案:
@Configuration public class ShiroConfig { //配置類的三大屬性 //一、shiroFilterFactoryBean @Bean public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager) { ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); //HashMap<String, Filter> filterHashMap = new HashMap<>(16); //filterHashMap.put("jwt", new ShiroFilter()); Map<String, Filter> filterMap = new HashMap<>(16); filterMap.put("jwt", new ShiroFilter()); bean.setFilters(filterMap); //設定安全管理器 bean.setSecurityManager(defaultWebSecurityManager); //新增shiro的內建過濾器 /** * anon:無需認證就可訪問 * authc:必須認證了才能訪問 * user:必須擁有,記住我,功能才能使用 * perms:擁有對某個資源的許可權才能訪問 * role:擁有某個角色許可權才能訪問 */ LinkedHashMap<String, String> filterChainMap = new LinkedHashMap<>(); // filterChainMap.put("/user/add", "authc"); // filterChainMap.put("/user/update", "authc"); // 可以使用萬用字元 // filterChainMap.put("/user/*", "authc"); filterChainMap.put("/base", "anon"); filterChainMap.put("/**", "jwt"); //普通使用者許可權 filterChainMap.put("/user/user", "perms[per:user]"); //管理員許可權 filterChainMap.put("/user/admin", "perms[per:admin]"); bean.setFilterChainDefinitionMap(filterChainMap); bean.setUnauthorizedUrl("/unauthor"); bean.setLoginUrl("/toLogin"); return bean; } //二、DefaultWebSecurityManager @Bean(name = "securityManager") public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); //關聯realm securityManager.setRealm(userRealm); return securityManager; } //三、建立realm 物件,需要自定義類 @Bean public UserRealm userRealm() { return new UserRealm(); } }
三、自定義ream
public class UserRealm extends AuthorizingRealm { private Logger logger = LoggerFactory.getLogger(this.getClass()); @Autowired private UserRoleService userRoleService; @Autowired private UserInfoMapper userInfoMapper; /** * Retrieves the AuthorizationInfo for the given principals from the underlying data store. When returning * an instance from this method, you might want to consider using an instance of * {@link SimpleAuthorizationInfo SimpleAuthorizationInfo}, as it is suitable in most cases. * * @param principals the primary identifying principals of the AuthorizationInfo that should be retrieved. * @return the AuthorizationInfo associated with this principals. * @see SimpleAuthorizationInfo */ // 授權 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { //授權操作 SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); Subject subject = SecurityUtils.getSubject(); //User user = (User) subject.getPrincipal(); UserInfo user = (UserInfo) subject.getPrincipal(); UserRole userRolePermission = userRoleService.getUserRolePermission(user.getUserEmail()); //List list = userService.queryUserPermissionsList(user.getUsername()); //subject.getPrincipal(); // 利用user物件,獲取user對應的相關許可權並加入到授權中 // Iterator iterator = list.iterator(); // while (iterator.hasNext()) { // String next = (String) iterator.next(); // info.addStringPermission(next); // } info.addStringPermission(userRolePermission.getPermission()); info.addStringPermission(userRolePermission.getRole()); return info; } /** * Retrieves authentication data from an implementation-specific datasource (RDBMS, LDAP, etc) for the given * authentication token. * <p/> * For most datasources, this means just 'pulling' authentication data for an associated subject/user and nothing * more and letting Shiro do the rest. But in some systems, this method could actually perform EIS specific * log-in logic in addition to just retrieving data - it is up to the Realm implementation. * <p/> * A {@code null} return value means that no account could be associated with the specified token. * * @param token the authentication token containing the user's principal and credentials. * @return an {@link AuthenticationInfo} object containing account data resulting from the * authentication ONLY if the lookup is successful (i.e. account exists and is valid, etc.) * @throws AuthenticationException if there is an error acquiring data or performing * realm-specific authentication logic for the specified <tt>token</tt> */ @Override public boolean supports(AuthenticationToken token) { return token instanceof UsernamePasswordToken; } // 認證 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { //獲取當前的使用者 UsernamePasswordToken userToken = (UsernamePasswordToken) token; // 封裝使用者的登入資料 UserInfo userInfo = userInfoMapper.selectByUserEmail(userToken.getUsername()); String s = Arrays.toString(userToken.getPassword()); if (userInfo == null) { throw new EventException(HttpStatus.BAD_REQUEST, "使用者不存在,請確認賬號是否正確!"); } else if (userInfo.getUserPassword().equals(s)) { logger.info("密碼校驗出錯!"); throw new EventException(HttpStatus.BAD_REQUEST, "密碼不匹配,請確認密碼正確!"); } //傳送認證資訊{principal:要義;credentials:證書;realmName:領域名稱} // new SimpleAuthenticationInfo(Object principal, Object credentials, String realmName) // 如果要把對應的使用者傳到授權的環節,就要在principal上放置user return new SimpleAuthenticationInfo(userInfo, userInfo.getUserPassword(), ""); } }
四、自定義過濾器
public class ShiroFilter extends BasicHttpAuthenticationFilter { private Logger logger = LoggerFactory.getLogger(this.getClass()); @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { //這裡只有返回false才會執行onAccessDenied方法,因為 // return super.isAccessAllowed(request, response, mappedValue); return false; } @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { //獲取請求token String token = getRequestToken((HttpServletRequest) request); String login = ((HttpServletRequest) request).getServletPath(); RedisTemplateService redisTemplateService = SpringUtils.getBean(RedisTemplateService.class); //判斷是否是通用的/base請求,不需要攔截 if (StringUtils.isMatch(login)) { logger.info("請求路徑為:" + login + ",不需要攔截"); return true; } //沒有token if (StringUtils.isEmpty(token)) { response.setCharacterEncoding("UTF-8"); response.setContentType("application/json; charset=utf-8"); AjaxResult ajaxResult = AjaxResult.error(HttpStatus.UNAUTHORIZED, "請先登入後再操作!"); String s = new ObjectMapper().writeValueAsString(ajaxResult); response.getWriter().print(s); logger.error("請求路徑==:" + login + "沒有token"); return false; } JWTUtil.verify(token); //從當前shiro中獲得使用者資訊 String userEmail = JWTUtil.getUserEmail(token); String userToken = redisTemplateService.get(userEmail); if (userToken.equals(token)) { //TODO 判斷token是否需要更新,如果需要就更新(視情況而定) //if (JWTUtil.isNeedUpdate(token)) { // String updateToken = JWTUtil.updateToken(token); // redisTemplateService.saveToken(userEmail, updateToken); //} logger.info("請求路徑==:" + login + "通過過濾"); return true; } else { response.setCharacterEncoding("UTF-8"); response.setContentType("application/json; charset=utf-8"); AjaxResult ajaxResult = AjaxResult.error(HttpStatus.UNAUTHORIZED, "登入已過期,請重新登入!"); String s = new ObjectMapper().writeValueAsString(ajaxResult); response.getWriter().print(s); logger.error("請求路徑==:" + login + "無效token"); } return false; } private String getRequestToken(HttpServletRequest request) { //預設從請求頭中獲得token return request.getHeader("Token"); } /** * Check if a given log record should be published. * * @param record a LogRecord * @return true if the log record should be published. */ }
五、引入token的工具類和方法實現
public class JWTUtil {
//設定的一個金鑰
private static final String USER_SRCRET = "booksalon";
public static final Date expireTime() {
//建立一個日曆
Calendar instance = Calendar.getInstance();
//預設令牌過期時間8小時
instance.add(Calendar.HOUR, 12);
return instance.getTime();
}
public static String updateToken(String update) {
try {
return JWT.create()
.withSubject(update)
.withExpiresAt(expireTime())
.sign(Algorithm.HMAC256(USER_SRCRET));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return null;
}
/**
* 獲取token
*
* @param u user
* @return token
*/
// public static String getToken(UserDetails u) {
// //建立一個日曆
// Calendar instance = Calendar.getInstance();
// //預設令牌過期時間8小時
// instance.add(Calendar.HOUR, 1);
//
// //建立JWT並在負載中加入使用者id,和電話
// JWTCreator.Builder builder = JWT.create();
// builder.withClaim("id", u.getUsername());
// //.withClaim("phone", u.getAuthorities());
//
// return builder.withExpiresAt(instance.getTime())
// .sign(Algorithm.HMAC256(USER_SRCRET));
//// return builder.sign(Algorithm.HMAC256(USER_SRCRET));
// }
//shiro
public static String getToken(UserInfo u) {
//建立一個日曆
Calendar instance = Calendar.getInstance();
//預設令牌過期時間8小時
instance.add(Calendar.HOUR, 12);
//建立JWT並在負載中加入使用者郵箱
JWTCreator.Builder builder = JWT.create();
builder.withClaim("userEmail", u.getUserEmail());
try {
return builder.withExpiresAt(instance.getTime())
.sign(Algorithm.HMAC256(USER_SRCRET));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return null;
// return builder.sign(Algorithm.HMAC256(USER_SRCRET));
}
/**
* 驗證token合法性 成功返回token
*/
public static DecodedJWT verify(String token) throws Exception {
if (token == null) {
throw new Exception("token不能為空");
}
JWTVerifier build = JWT.require(Algorithm.HMAC256(USER_SRCRET)).build();
return build.verify(token);
}
//獲取token中的userEmail
public static String getUserEmail(String token) throws Exception {
DecodedJWT verify = verify(token);
return verify.getClaim("userEmail").asString();
}
/**
* 檢查token是否需要更新
*
* @param token
* @return
*/
public static boolean isNeedUpdate(String token) {
//獲取token過期時間
Date expiresAt = null;
try {
expiresAt = JWT.require(Algorithm.HMAC256(USER_SRCRET))
.build()
.verify(token)
.getExpiresAt();
} catch (TokenExpiredException e) {
return true;
} catch (Exception e) {
throw new RuntimeException("token驗證失敗");
}
//如果剩餘過期時間少於過期時常的一般時 需要更新
return (expiresAt.getTime() - System.currentTimeMillis()) / 1000 / 60 / 60 < 3;
}
/* public static void main(String[] args) {
DecodedJWT verify = verify("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MTcxMDg1MDAsInVzZXJuYW1lIjoiYWRtaW4ifQ.geBEtpluViRUg66_P7ZisN3I_d4e32Wms8mFoBYM5f0");
System.out.println(verify.getClaim("password").asString());
}*/
}
六、使用者接入shiro登入,subject是一個全域性可用的物件
Subject subject = SecurityUtils.getSubject();
try {
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(userInfo.getUserEmail(),
StringUtils.passwordMd5(userInfo.getUserPassword()));
subject.login(usernamePasswordToken);
} catch (Exception e) {
return AjaxResult.error(e.getMessage());
}
需要引入的包:
<!--shiro 鑑權和授權導包-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.7.1</version>
</dependency>
<!-- jwt-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.3.0</version>
</dependency>