技術棧:Spring boot + Shiro +JWT
先說一下 Spring security 和 Shiro ,從這兩者選擇的時候最後還是選擇了Shiro,原因是Spring security 偏重,適合大型企業項目,而且現在用Shiro的也不少。網上這兩個的對比文章還是很多的,這裏就不贅述。
Shiro默認實現的是session形式,也就是有狀態的。我們要改變一些東西,來實現無狀態的RESTful 架構~
1. pom.xml
只需要加入這兩個包就可以
<!-- shiro -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<!-- JWT -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.8.0</version>
</dependency>
2. JWT配置
1. JWTToken.java 實體類
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;
}
}
2. JWTUtil.java 來實現JWT的token解析等工具類
public class JWTUtil {
// 過期時間一小時
private static final long EXPIRE_TIME = 60*60*1000;
private static final Logger PLOG = LoggerFactory.getLogger(JWTUtil.class);
/**
* 校驗token是否正確
* @param token TOKEN
* @param secret 用戶的密碼
* @return boolean
*/
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 e) {
PLOG.error("JWT >> " + e);
return false;
}
}
/**
* 無需解密直接獲得token中的用戶名
* @return token中包含的用戶名
*/
public static String getUsername(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("username").asString();
} catch (JWTDecodeException e) {
PLOG.error("JWT >> " + e);
return null;
}
}
/**
* 生成簽名,並設置過期時間
* @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) {
PLOG.error("JWT >> " + e);
return null;
}
}
}
3. JWTFilter.java 我們要加一個我們自己的Shiro過濾器,並配置在Shiro
public class JWTFilter extends BasicHttpAuthenticationFilter {
private static final Logger PLOG = LoggerFactory.getLogger(JWTFilter.class);
/**
* 判斷用戶是否想要登入。
* 檢測header裏面是否包含Authorization字段即可
*/
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
HttpServletRequest req = (HttpServletRequest) request;
String authorization = req.getHeader("Authorization");
return true;
}
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String authorization = httpServletRequest.getHeader("Authorization");
JWTToken token = new JWTToken(authorization);
// 提交給realm進行登入,如果錯誤他會拋出異常並被捕獲
getSubject(request, response).login(token);
// 如果沒有拋出異常則代表登入成功,返回true
return true;
}
/**
* 這裏控制通過與否
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
if (isLoginAttempt(request, response)) {
try {
executeLogin(request, response);
} catch (Exception e) {
PLOG.error("JWT >> " + e);
responseError(request, response);
return false;
}
}
return true;
}
/**
* 將非法請求跳轉到 /ign/error
*/
private void responseError(ServletRequest req, ServletResponse resp) {
try {
HttpServletResponse httpServletResponse = (HttpServletResponse) resp;
httpServletResponse.sendRedirect("/user/ign/error");
} catch (IOException e) {
PLOG.error("JWT >> " + e);
}
}
}
3. Shiro配置
CustomRealm.java
doGetAuthenticationInfo中拋出異常來進行身份判定
public class CustomRealm extends AuthorizingRealm {
private UserMapper userMapper;
private static final Logger PLOG = LoggerFactory.getLogger(CustomRealm.class);
@Autowired
private void setUserMapper(UserMapper userMapper) {
this.userMapper = userMapper;
}
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JWTToken;
}
/**
* 獲取身份驗證信息
* Shiro中,最終是通過 Realm 來獲取應用程序中的用戶、角色及權限信息的。
*
* @param authenticationToken 用戶身份信息 token
* @return 返回封裝了用戶信息的 AuthenticationInfo 實例
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
PLOG.info("Shiro >> 身份認證");
String token = (String) authenticationToken.getCredentials();
if (token == null) {
throw new AuthenticationException("token invalid");
}
String username = JWTUtil.getUsername(token);
if (username == null) {
throw new AuthenticationException("token invalid");
}
// 從數據庫獲取對應用戶名密碼的用戶
String password = userMapper.getPasswordByUsername(username);
if (null == password) {
throw new AuthenticationException("User didn't existed!");
}
if (!JWTUtil.verify(token, username, password)) {
throw new AuthenticationException("Username or password error");
}
return new SimpleAuthenticationInfo(token, token, getName());
}
/**
* 獲取授權信息
*
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
PLOG.info("Shiro >> 權限認證");
String username = JWTUtil.getUsername(principalCollection.toString());
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//獲得該用戶角色
Integer power = userMapper.getPowerByUsername(username);
Set<String> set = new HashSet<>();
if (power == 100) {
set.add("admin");
}
set.add("user");
info.setRoles(set);
return info;
}
}
ShiroConfig.java
從這裏加上我們上文寫過的JWTFilter 以及配置一下權限控制
@Configuration
public class ShiroConfig {
private static final Logger PLOG = LoggerFactory.getLogger(ShiroConfig.class);
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 必須設置 SecurityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 自定義過濾器
Map<String, Filter> filterMap = new HashMap<>();
filterMap.put("jwt", new JWTFilter());
shiroFilterFactoryBean.setFilters(filterMap);
shiroFilterFactoryBean.setLoginUrl("/user/ign/notLogin");
shiroFilterFactoryBean.setUnauthorizedUrl("/user/ign/notRole");
// 設置攔截器
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// 開放登錄、未登錄等映射
filterChainDefinitionMap.put("/user/ign/**", "anon");
// 攔截接口
filterChainDefinitionMap.put("/user/**", "jwt");
// 其餘接口一律攔截
// filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
PLOG.info("Shiro >> Shiro攔截器工廠類注入成功");
return shiroFilterFactoryBean;
}
/**
* 注入 securityManager
*/
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 設置 realm.
securityManager.setRealm(customRealm());
// 關閉 shiro 自帶的session
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
return securityManager;
}
/**
* 自定義身份認證 realm;
* <p>
* 必須寫這個類,並加上 @Bean 註解,目的是注入 CustomRealm,
* 否則會影響 CustomRealm類 中其他類的依賴注入
*/
@Bean
public CustomRealm customRealm() {
return new CustomRealm();
}
/**
* 下面的代碼是添加註解支持
*/
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
// 強制使用cglib,防止重複代理和可能引起代理出錯的問題
// 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;
}
}
4. 登錄
登錄的Controller 中,生成token並返給前端使用
@PostMapping(value = "/ign/login")
public ServerResponse<String> login( @RequestBody User user) {
User sourceUser = userService.getUserByUsername(user.getUsername());
String md5Pass = Analysis.knoveMD5(user.getPassword());
if (sourceUser.getPassword().equals(md5Pass)) {
PLOG.info("UserController >> login · 獲取Token");
return ServerResponse.createBySuccess(JWTUtil.sign(user.getUsername(), md5Pass));
}
return ServerResponse.createByError("登錄失敗!");
}