一、先了解攔截器在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>