shiro
簡介
- Subject:主體
- 代表了目前 “使用者”,這個使用者不一定是一個具體的人,與目前應用互動的任何東西都是 Subject,如網絡爬蟲,機器人等;即一個抽象概念;所有 Subject 都綁定到 SecurityManager,與 Subject 的所有互動都會委托給 SecurityManager;可以把 Subject 認為是一個門面;
- SecurityManager:安全管理器
- 即所有與安全有關的操作都會與 SecurityManager 互動;且它管理着所有 Subject;可以看出它是 Shiro 的核心,它負責與後邊介紹的其他元件進行互動,如果學習過 SpringMVC,你可以把它看成 DispatcherServlet 前端控制。
- Realms:域
- Shiro 從從 Realm 擷取安全資料(如使用者、角色、權限),就是說 SecurityManager 要驗證使用者身份,那麼它需要從 Realm 擷取相應的使用者進行比較以确定使用者身份是否合法;也需要從 Realm 得到使用者相應的角色 / 權限進行驗證使用者是否能進行操作;可以把 Realm 看成 DataSource,即安全資料源。
登入認證代碼實作
pom
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.2</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
<version>1.18.12</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.20</version>
</dependency>
<!-- shiro -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>${shiro-version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>${shiro-version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>${shiro-version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
實體
使用者實體
@Data
@TableName("sys_user")
public class SysUser {
private int id;
private String username;
private String password;
private int status;
private int isDelete;
}
使用者資料庫
id | username | password | status | is_delete |
---|---|---|---|---|
1 | admin | 123 | ||
2 | aaa | 82c1a1ef7dd57d095f3d221e51bd6b16 |
自定義realm
public class MyRealm extends AuthorizingRealm {
@Autowired
private SysUserService userService;
/**
* 授權
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
/**
* 登入認證
* @param authenticationToken 封裝的token(UsernamePasswordToken)
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
if (authenticationToken.getPrincipal() == null) {
throw new AuthenticationException("token不合法");
}
String username = authenticationToken.getPrincipal().toString();
log.info("使用者名:{}", username);
SysUser one = userService.getOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getUsername, username));
if (one == null) {
// 沒有該使用者名
log.error("沒有該使用者名: {}", username);
throw new AuthenticationException("使用者不存在");
}
// 使用者狀态判斷
if (one.getStatus() == 1) {
throw new AccountException("使用者被禁用");
}
if (one.getIsDelete() == 1) {
throw new AccountException("使用者被删除");
}
// 其他業務判斷
// 判斷通過後,将資料庫中查詢出來的user封裝為info
return new SimpleAuthenticationInfo(
one.getUsername(),// 這個參數是什麼,在後續的subject.getPrincipal就是什麼,也可以設定使用者實體
one.getPassword(),
// ByteSource.Util.bytes(one.getUsername()),// 密碼加密的"鹽值",可以是username、id等
getName()
);
}
}
shiro配置類
public class ShiroConfig {
@Bean("credentialsMatcher")
public HashedCredentialsMatcher credentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
// 密碼加密算法
hashedCredentialsMatcher.setHashAlgorithmName("MD5");
// 加密次數
hashedCredentialsMatcher.setHashIterations(512);
return hashedCredentialsMatcher;
}
/**
* 自定義realm
*/
@Bean("myRealm")
public MyRealm myRealm(@Qualifier("credentialsMatcher") HashedCredentialsMatcher credentialMatcher) {
MyRealm myRealm = new MyRealm();
// myRealm.setCredentialsMatcher(credentialMatcher);
return myRealm;
}
@Bean("securityManager")
public SecurityManager securityManager(@Qualifier("myRealm") MyRealm myRealm) {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
manager.setRealm(myRealm);
/*
* 關閉shiro自帶的session,詳情見文檔
*/
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
manager.setSubjectDAO(subjectDAO);
return manager;
}
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager") SecurityManager securityManager) {
ShiroFilterFactoryBean filter = new ShiroFilterFactoryBean();
filter.setSecurityManager(securityManager);
LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// 路徑比對的順序就是put進去的順序(最先比對原則)
// login接口不需要認證
filterChainDefinitionMap.put("/auth/login", "anon");
// getInfo需要認證
filterChainDefinitionMap.put("/auth/getInfo", "authc");
filterChainDefinitionMap.put("/**", "authc");
filter.setLoginUrl("/auth/login");
filter.setSuccessUrl("/auth/getInfo");
filter.setUnauthorizedUrl("/auth/error");
filter.setFilterChainDefinitionMap(filterChainDefinitionMap);
return filter;
}
/**注冊shiro的Filter 攔截請求*/
@Bean
public FilterRegistrationBean<Filter> filterRegistrationBean(SecurityManager securityManager) throws Exception {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter((Filter) Objects.requireNonNull(this.shiroFilterFactoryBean(securityManager).getObject()));
filterRegistrationBean.addInitParameter("targetFilterLifecycle","true");
//bean注入開啟異步方式
filterRegistrationBean.setAsyncSupported(true);
filterRegistrationBean.setEnabled(true);
filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST);
return filterRegistrationBean;
}
/**
* shiro聲明周期
* @return
*/
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
// 以下配置開啟shiro注解(@RequiresPermissions)
@Bean
@DependsOn({"lifecycleBeanPostProcessor"})
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
// 強制使用cglib,防止重複代理和可能引起代理出錯的問題
// https://zhuanlan.zhihu.com/p/29161098
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
/**
* 啟用shiro注解
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
登入業務
public class AuthServiceImpl implements AuthService {
@Override
public String login(SysUser sysUser) {
// 非空判斷
if (sysUser == null) {
return null;
}
if (sysUser.getUsername() == null || sysUser.getPassword() == null) {
return null;
}
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(
sysUser.getUsername(),
sysUser.getPassword()
);
try {
subject.login(usernamePasswordToken);
} catch (AuthenticationException e) {
log.error("登入失敗");
throw new AuthenticationException("登入失敗");
}
return "登入成功";
}
}
接口
@RestController
@RequestMapping("auth")
public class AuthController {
@Autowired
private AuthService authService;
@PostMapping("login")
public String login(@RequestBody SysUser sysUser) {
return authService.login(sysUser);
}
@GetMapping("getInfo")
public String getInfo() {
return SecurityUtils.getSubject().getPrincipal().toString();
}
@RequestMapping("error")
public String error() {
return "fail";
}
}
測試

當直接通路
auth/getInfo
時,可以看到,無法通路該接口,并且會自動跳轉到登入接口(
auth/login
)。
使用使用者名和密碼登入
然後再通路
auth/getInfo
接口
現在可以正确擷取到資訊,可以看到擷取到的
SecurityUtils.getSubject().getPrincipal()
,就是在reamlm中傳回的info設定的
principal
參數。
注:如果要使用加密,則要在shiro配置類裡,給自定義的realm設定密碼比對器
@Bean("credentialsMatcher")
public HashedCredentialsMatcher credentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
// 密碼加密算法
hashedCredentialsMatcher.setHashAlgorithmName("MD5");
// 加密次數
hashedCredentialsMatcher.setHashIterations(512);
return hashedCredentialsMatcher;
}
/**
* 自定義realm
*/
@Bean("myRealm")
public MyRealm myRealm(@Qualifier("credentialsMatcher") HashedCredentialsMatcher credentialMatcher) {
MyRealm myRealm = new MyRealm();
myRealm.setCredentialsMatcher(credentialMatcher);
return myRealm;
}
自定義realme裡面最後傳回的info需要帶上加密的“鹽值”
return new SimpleAuthenticationInfo(
one.getUsername(),
one.getPassword(),
ByteSource.Util.bytes(one.getUsername()),
getName()
);
總結
登入認證的流程:
- 将前端傳來的使用者名和密碼封裝成
UsernamePasswordToken
- 調用Subject的
方法,實際上是調用的login(UsernamePasswordToken)
的login方法SecurityManager
- 進入自定義realme方法
- 最終由定義的密碼比對器進行密碼比對
SimpleCredentialsMatcher源碼:預設使用該比對器,即不加密
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
Object tokenCredentials = this.getCredentials(token);
Object accountCredentials = this.getCredentials(info);
return this.equals(tokenCredentials, accountCredentials);
}
// token:前端封裝的使用者名密碼token
// info:realme中傳回的info
實際就是把資料庫中的密碼和前端輸入的密碼進行對比
HashedCredentialsMatcher源碼:加密的密碼比對器
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
Object tokenHashedCredentials = this.hashProvidedCredentials(token, info);
Object accountCredentials = this.getCredentials(info);
return this.equals(tokenHashedCredentials, accountCredentials);
}
protected Object hashProvidedCredentials(AuthenticationToken token, AuthenticationInfo info) {
Object salt = null;
if (info instanceof SaltedAuthenticationInfo) {
salt = ((SaltedAuthenticationInfo)info).getCredentialsSalt();
} else if (this.isHashSalted()) {
salt = this.getSalt(token);
}
return this.hashProvidedCredentials(token.getCredentials(), salt, this.getHashIterations());
}
// 在這裡實作的加密
protected Hash hashProvidedCredentials(Object credentials, Object salt, int hashIterations) {
String hashAlgorithmName = this.assertHashAlgorithmName();
return new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations);
}
把前端輸入的明文密碼,用設定的鹽值和加密次數進行加密後,再與資料庫中的密文密碼進行對比。
授權代碼實作
授權就需要實作自定義realme中的
doGetAuthorizationInfo
方法
資料庫
sys_role
id | name | code | staus | Is_delete |
---|---|---|---|---|
1 | 管理者 | admin | ||
2 | 作家 | writer |
sys_permission
id | name | code | url | status | Is_delete |
---|---|---|---|---|---|
1 | 使用者檢視 | user:view | /user/** | ||
2 | 使用者增删改 | user:edit | /user/** | ||
3 | 文章檢視 | article:view | /article/** | ||
4 | 文章增删改 | article:edit | /article/** |
另外還有使用者角色關聯表和角色權限關聯表
我這裡測試的資料是:
admin使用者是管理者和作家,aaa使用者是作家。
管理者擁有所有權限,作家擁有文章相關的權限。
修改自定義realme
/**
* 授權
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
String username = principalCollection.getPrimaryPrincipal().toString();
SysUser sysUser = userService.getOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getUsername, username));
// 去資料庫中查詢該使用者的角色和權限
List<SysRole> roles = roleService.getRoles(sysUser.getId());
List<SysPermission> permissions = permissionService.getPermissions(sysUser.getId());
Set<String> permissionSet = new HashSet<>();
Set<String> roleSet = new HashSet<>();
permissions.forEach(item -> permissionSet.add(item.getCode()));
roles.forEach(item -> roleSet.add(item.getCode()));
authorizationInfo.addRoles(roleSet);
authorizationInfo.addStringPermissions(permissionSet);
return authorizationInfo;
}
當遇到需要鑒權的時候,會走
doGetAuthorizationInfo
方法
測試
接口
@RequiresPermissions("user:view")
@GetMapping("userView")
public String userView() {
return "使用者檢視";
}
@RequiresPermissions("article:view")
@GetMapping("articleView")
public String articleView() {
return "文章檢視";
}
@RequiresRoles("admin")
@GetMapping("admin")
public String admin() {
return "管理者";
}
登入使用者是
aaa
的時候,隻能通路
articleView
接口,其他接口均無相應的權限。
通路articleView接口,可以成功通路
當通路其他兩個接口時,控制台報錯無權調用此方法
org.apache.shiro.authz.AuthorizationException: Not authorized to invoke method
整合JWT
準備工作
pom
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.3</version>
</dependency>
jwt工具類
建立、解析token
@Slf4j
@Component
public class JwtUtil {
private static final String secret = "secret";
/**
* 建立token
*/
public static String createToken(String username, Long time) throws UnsupportedEncodingException {
long expiration = System.currentTimeMillis() + time;
Date expireDate = new Date(expiration);
String token = JWT.create()
.withClaim("sys_username", username)
.withExpiresAt(expireDate)
.sign(Algorithm.HMAC256(secret));
log.info("使用者:{} =====> token:{}", username, token);
return token;
}
/**
* 校驗token是否正确
*/
public static boolean verify(String token, String username) throws UnsupportedEncodingException, TokenExpiredException {
Algorithm algorithm = Algorithm.HMAC256(secret);
JWTVerifier verifier = JWT.require(algorithm)
.withClaim("sys_username", username)
.build();
verifier.verify(token);
return true;
}
/**
* 解析token,擷取使用者名
*/
public static String getUsername(String token) {
DecodedJWT decode = JWT.decode(token);
return decode.getClaim("sys_username").asString();
}
}
自定義token
用自定義的token取代shiro中的token,例如前面的UsernamePasswordToken
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;
}
}
自定義密碼比對器
public class JwtCredentialsMatcher extends HashedCredentialsMatcher {
/**
* @param info realme中傳回的是username,是以getPrincipals()擷取的是使用者名
*/
@Override
public boolean doCredentialsMatch(AuthenticationToken authenticationToken, AuthenticationInfo info) {
JwtToken jwtToken = (JwtToken) authenticationToken;
String token = jwtToken.getCredentials().toString();
try {
return JwtUtil.verify(token, info.getPrincipals().toString());
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("token解析失敗");
} catch (TokenExpiredException e) {
throw new RuntimeException("token過期");
}
}
}
自定義過濾器
public class JwtFilter extends AccessControlFilter {
/**
* 對跨域提供支援
*/
@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"));
// 跨域時會首先發送一個option請求,這裡我們給option請求直接傳回正常狀态
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
/**
* 是否允許通路
* isAccessAllowed傳回false後,去執行onAccessDenied方法
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws UnsupportedEncodingException {
Subject subject = SecurityUtils.getSubject();
String token = HttpUtil.getToken((HttpServletRequest) request);
if (StringUtils.isNotBlank(token)) {
JwtToken jwtToken = new JwtToken(token);
try {
subject.login(jwtToken);
return true;// 登入成功
} catch (Exception e) {
// 登入失敗
throw new RuntimeException("登入失敗");
}
} else {
// 沒有token
return false;
}
}
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
String token = HttpUtil.getToken((HttpServletRequest) servletRequest);
if (StringUtils.isNotBlank(token)) {
String username = JwtUtil.getUsername(token);
if (JwtUtil.verify(token, username)) {
// 沒有權限
}
} else {
// 沒有token
}
return false;
}
}
修改shiro配置類
@Bean("jwtFilter")
public JwtFilter jwtFilter() {
return new JwtFilter();
}
@Bean("credentialsMatcher")
public JwtCredentialsMatcher credentialsMatcher() {
return new JwtCredentialsMatcher();
}
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager") SecurityManager securityManager) {
ShiroFilterFactoryBean filter = new ShiroFilterFactoryBean();
filter.setSecurityManager(securityManager);
Map<String, Filter> filterMap = new LinkedHashMap<>();
filterMap.put("jwt", new JwtFilter());
filter.setFilters(filterMap);
LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// login接口不需要認證
filterChainDefinitionMap.put("/auth/login", "anon");
filterChainDefinitionMap.put("/**", "jwt");
filter.setFilterChainDefinitionMap(filterChainDefinitionMap);
return filter;
}
加入自定義的過濾器和密碼比對器,所有接口都需要執行自定義過濾器。
修改自定義realme
@Slf4j
public class MyRealm extends AuthorizingRealm {
@Autowired
private SysUserService userService;
@Autowired
private SysRoleService roleService;
@Autowired
private SysPermissionService permissionService;
// 不寫該方法,會報錯不支援自定義的token
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
/**
* 授權
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
String username = principalCollection.getPrimaryPrincipal().toString();
SysUser sysUser = userService.getOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getUsername, username));
// 去資料庫中查詢該使用者的角色和權限
List<SysRole> roles = roleService.getRoles(sysUser.getId());
List<SysPermission> permissions = permissionService.getPermissions(sysUser.getId());
Set<String> permissionSet = new HashSet<>();
Set<String> roleSet = new HashSet<>();
permissions.forEach(item -> permissionSet.add(item.getCode()));
roles.forEach(item -> roleSet.add(item.getCode()));
authorizationInfo.addRoles(roleSet);
authorizationInfo.addStringPermissions(permissionSet);
return authorizationInfo;
}
/**
* 登入認證
* @param authenticationToken 自定義的token
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// authenticationToken就是JwtToken
if (authenticationToken.getPrincipal() == null) {
throw new AuthenticationException("token不合法");
}
String token = authenticationToken.getPrincipal().toString();
String username = JwtUtil.getUsername(token);
SysUser one = userService.getOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getUsername, username));
if (one == null) {
// 沒有該使用者名
log.error("沒有該使用者名: {}", username);
throw new AuthenticationException("使用者不存在");
}
// 使用者狀态判斷
if (one.getStatus() == 1) {
throw new AccountException("使用者被禁用");
}
if (one.getIsDelete() == 1) {
throw new AccountException("使用者被删除");
}
// 其他業務判斷
// 判斷通過後,将資料庫中查詢出來的user封裝為info
return new SimpleAuthenticationInfo(
one.getUsername(),// 這個參數是什麼,在後續的subject.getPrincipal就是什麼
one.getPassword(),
ByteSource.Util.bytes(one.getUsername()),// 密碼加密的"鹽值",可以是username、id等
getName()
);
}
}
修改登入代碼
@Override
public String login(SysUser sysUser) {
// 非空判斷
if (sysUser == null) {
return null;
}
if (sysUser.getUsername() == null || sysUser.getPassword() == null) {
return null;
}
try {
String token = JwtUtil.createToken(sysUser.getUsername(), 1440000L);
Subject subject = SecurityUtils.getSubject();
JwtToken jwtToken = new JwtToken(token);
subject.login(jwtToken);
return token;
} catch (AuthenticationException e) {
log.error("登入失敗");
throw new AuthenticationException("登入失敗");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return "登入失敗";
}
測試
登入成功後,會傳回token。
攜帶token調用接口,成功傳回資料
通路不具備權限的接口
控制台報錯:
org.apache.shiro.authz.AuthorizationException: Not authorized to invoke method: public java.lang.String com.shiro.controller.AuthController.userView()
總結
到此,shiro部分和shiro整合jwt部分完成。這隻是一個例子,代碼中還有很多需要完善的地方,比如:傳回結果的封裝、對異常的處理等。
另外一般在系統中,token過期重新整理也是必不可少的。