說明:前後端不分離的時候springboot+shiro可以實作有狀态服務,前後端分離後工程就變成無狀态服務,本文直接代碼解決工程無狀态問題。
注:
1.了解jwt的使用
2.文章的異常為自定義異常,粘貼代碼的時候可以改為runtime異常!
3.文章中的重要内容已标紅
一:前期準備工作
a.引入maven
<!--token驗證 jwt-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
<!--整合權限-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>1.2.1</version>
</dependency>
b.引入實體
使用者類
@Setter
@Getter
@TableName("t_user")
public class User {
@TableId(type = IdType.UUID)
private String id;
private String nickName;//使用者昵稱
private String userName;//使用者名
private String passWord;//mima
private String tele;//電話
private Date createTime;//建立時間
@TableField(exist = false)
private List<Role> roleList;//使用者有的角色
@TableField(exist = false)
private List<Res> resList;//使用者有的權限
}
角色實體
@Setter
@Getter
@TableName("t_role")
public class Role {
@TableId(type = IdType.UUID)
private String id;
private String roleName;//角色名稱
private String roleStatus;//角色狀态 0未啟用 1啟用
private Date createTime;//建立時間
}
菜單實體
@Setter
@Getter
@TableName("t_res")
public class Res {
@TableId(type = IdType.UUID)
private String id;
private String resName;//資源名稱名稱
private String resUrl;//資源路徑
private String resCode;//資源辨別
private String isMenu;//是否是按鈕 0菜單 1按鈕
private String icon;//菜單樣式
private String pid;//父級資源
private Date createTime;//建立時間
/**
* 按鈕需要資料
* @return {@link }
* @throws
* @author 李慶偉
* @date 2020/5/8 10:14
*/
@TableField(exist = false)
private String menuIcon = null;//按鈕圖示
@TableField(exist = false)
private int checked = 0;//是否勾選checkbox
/**
* 左側菜單需要資料
* @return {@link }
* @throws
* @author 李慶偉
* @date 2020/5/8 10:14
*/
@TableField(exist = false)
private String target = "_self";//菜單樣式
@TableField(exist = false)
private List<my.expt.model.Res> child ;//菜單下子菜單
public String getTitle() {
return resName;
}
public String getHref() {
return resUrl;
}
/*
@TableField(exist = false)
private Map<String,String> homeInfo ;//首頁菜單 預設一直有
@TableField(exist = false)
private Map<String,String> logoInfo ;//LAYUI MINI菜單 預設一直有
@TableField(exist = false)
private Map<String,String> menuInfo ;//菜單封裝 預設一直有
*/
}
二:TokenUtils工具類(其實就是jwt)
@Component
public class TokenUtil {
public static String key = "this is a jwt project";
//public static long ttlMillis = 5000;//設定過期時間
public static long ttlMillis = 30*60*100000;//設定過期時間
/**
* 使用者登入成功後生成Jwt
* 使用Hs256算法 私匙使用使用者mima
*
* @param user 登入成功的user對象
* @return
*/
public static String createJWT(User user) {
//指定簽名的時候使用的簽名算法,也就是header那部分,jjwt已經将這部分内容封裝好了。
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
//生成JWT的時間
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
//建立payload的私有聲明(根據特定的業務需要添加,如果要拿這個做驗證,一般是需要和jwt的接收方提前溝通好驗證方式的)
Map<String, Object> claims = new HashMap<String, Object>();
claims.put("user",user);
claims.put("userName",user.getUserName());
claims.put("passWord",user.getPassWord());
/*claims.put("roleList",user.getRoleList() == null || user.getRoleList().size() == 0 ? new ArrayList<Role>():user.getRoleList());
claims.put("resList",user.getResList() == null || user.getResList().size() == 0 ? new ArrayList<Res>() :user.getResList());*/
if(user.getResList() != null || user.getResList().size() > 0 ){
StringBuffer sb = new StringBuffer();
for (int a = 0; a< user.getResList().size(); a++){
if(a != user.getResList().size()-1){
sb.append(user.getResList().get(a).getResCode()).append(",");
} else {
sb.append(user.getResList().get(a).getResCode());
}
}
claims.put("resList",sb.toString());
}
//生成簽名的時候使用的秘鑰secret,這個方法本地封裝了的,一般可以從本地配置檔案中讀取,切記這個秘鑰不能外露哦。它就是你服務端的私鑰,在任何場景都不應該流露出去。一旦用戶端得知這個secret, 那就意味着用戶端是可以自我簽發jwt了。
//生成簽發人
String subject = user.getUserName();
//下面就是在為payload添加各種标準聲明和私有聲明了
//這裡其實就是new一個JwtBuilder,設定jwt的body
JwtBuilder builder = Jwts.builder()
//如果有私有聲明,一定要先設定這個自己建立的私有的聲明,這個是給builder的claim指派,一旦寫在标準的聲明指派之後,就是覆寫了那些标準的聲明的
.setClaims(claims)
//設定jti(JWT ID):是JWT的唯一辨別,根據業務需要,這個可以設定為一個不重複的值,主要用來作為一次性token,進而回避重播攻擊。
.setId(UUID.randomUUID().toString())
//iat: jwt的簽發時間
.setIssuedAt(now)
//代表這個JWT的主體,即它的所有人,這個是一個json格式的字元串,可以存放什麼userid,roldid之類的,作為什麼使用者的唯一标志。
.setSubject(subject)
//設定簽名使用的簽名算法和簽名使用的秘鑰
.signWith(signatureAlgorithm, key);
if (ttlMillis >= 0) {
long expMillis = nowMillis + ttlMillis;
Date exp = new Date(expMillis);
//設定過期時間
builder.setExpiration(exp);
}
return builder.compact();
}
/**
* Token的jiemi
* @param token 加密後的token
* @param
* @return
*/
public static Claims parseJWT(String token) {
//得到DefaultJwtParser
Claims claims = Jwts.parser()
//設定簽名的秘鑰
.setSigningKey(key)
//設定需要解析的jwt
.parseClaimsJws(token).getBody();
return claims;
}
/**
* 校驗token
* 在這裡可以使用官方的校驗,我這裡校驗的是token中攜帶的mima于資料庫一緻的話就校驗通過
* @param token
* @return
*/
public static Boolean isVerify(String token) {
//得到DefaultJwtParser
Claims claims = Jwts.parser()
//設定簽名的秘鑰
.setSigningKey(key)
//設定需要解析的jwt
.parseClaimsJws(token).getBody();
if ((System.currentTimeMillis()-claims.getIssuedAt().getTime())<ttlMillis) {
return true;
}
throw new MyException(ResultEnum.USER_LOTIN_TIME_OUT.getExpKey(), ResultEnum.USER_LOTIN_TIME_OUT.getExpValue());
}
}
三:自定義攔截器(取代原來shiro的攔截校驗規則)
/**
* @author 李慶偉
* @date 2020/7/11 13:57
*/
@Slf4j
public class CustomAuthorizationFilter extends BasicHttpAuthenticationFilter {
private static final String TOKEN = "Authentication";
/**
* 判斷使用者是否想要登入。
* 檢測header裡面是否包含Authorization字段即可
*/
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
HttpServletRequest req = (HttpServletRequest) request;
String authorization = req.getHeader(TOKEN);
return authorization != null;
}
/**
*
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String authorization = httpServletRequest.getHeader(TOKEN);
JwtToken token = new JwtToken(authorization);
// 送出給realm進行登入,如果錯誤他會抛出異常并被捕獲
getSubject(request, response).login(token);
// 如果沒有抛出異常則代表登入成功,傳回true
return true;
}
/**
* 這裡我們詳細說明下為什麼最終傳回的都是true,即允許通路
* 例如我們提供一個位址 GET /article
* 登入使用者和遊客看到的内容是不同的
* 如果在這裡傳回了false,請求會被直接攔截,使用者看不到任何東西
* 是以我們在這裡傳回true,Controller中可以通過 subject.isAuthenticated() 來判斷使用者是否登入
* 如果有些資源隻有登入使用者才能通路,我們隻需要在方法上面加上 @RequiresAuthentication 注解即可
* 但是這樣做有一個缺點,就是不能夠對GET,POST等請求進行分别過濾鑒權(因為我們重寫了官方的方法),但實際上對應用影響不大
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
if (isLoginAttempt(request, response)) {
try {
executeLogin(request, response);
} catch (Exception e) {
response401(request, response);
}
}
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"));
// 跨域時會首先發送一個option請求,這裡我們給option請求直接傳回正常狀态
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
/**
* 将非法請求跳轉到 /401
*/
private void response401(ServletRequest req, ServletResponse resp) {
try {
HttpServletResponse httpServletResponse = (HttpServletResponse) resp;
httpServletResponse.sendRedirect("/401");
} catch (IOException e) {
log.error(e.getMessage());
}
}
}
四:重寫原有的
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(userName,passWord);
/**
* @author 李慶偉
* @date 2020/7/11 14:33
*/
public class JwtToken implements AuthenticationToken {
private static final long serialVersionUID = 1282057025599826155L;
private String token;
private String exipreAt;
public JwtToken(String token) {
this.token = token;
}
public JwtToken(String token, String exipreAt) {
this.token = token;
this.exipreAt = exipreAt;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
五:shiro的配置類
/**
* @author 李慶偉
* @date 2020/4/23 10:54
*/
@Configuration
public class ShiroConfiguration {
//不加這個注解不生效,具體不詳
@Bean
@ConditionalOnMissingBean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
defaultAAP.setProxyTargetClass(true);
return defaultAAP;
}
//将自己的驗證方式加入容器
@Bean
public MyShiroRealm myShiroRealm() {
MyShiroRealm myShiroRealm = new MyShiroRealm();
return myShiroRealm;
}
//權限管理,配置主要是Realm的管理認證
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myShiroRealm());
return securityManager;
}
//Filter工廠,設定對應的過濾條件和跳轉條件
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map<String, Filter> filters = new HashMap<>();
//添加自定義過濾器
filters.put("jwt", new CustomAuthorizationFilter());
shiroFilterFactoryBean.setFilters(filters);
//登入
shiroFilterFactoryBean.setLoginUrl("/user/login");
Map<String,String> map = new LinkedHashMap<String, String>();
map.put("/swagger-ui.html", "anon");//swagger
map.put("/webjars/**", "anon");
map.put("/v2/**", "anon");
map.put("/swagger-resources/**", "anon");//swagger
map.put("/**","jwt");//對所有使用者認證
//map.put("/**/**", "anon");
//錯誤頁面,認證不通過跳轉
shiroFilterFactoryBean.setUnauthorizedUrl("/error");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
//加入注解的使用,不加入這個注解不生效
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
六:Realm授權認證
/**
* @author 李慶偉
* @date 2020/4/23 10:55
*/
public class MyShiroRealm extends AuthorizingRealm {
//用于使用者查詢
@Autowired
private UserService userService;
/**
* 必須重寫此方法,不然Shiro會報錯
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
//角色權限和對應權限添加
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//擷取登入使用者名
//User user = (User) principals.getPrimaryPrincipal();
String token = principals.toString();
//添加角色和權限
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
if(StringUtils.isEmpty(token)){ //如果使用者未登入傳回沒有權限
return simpleAuthorizationInfo;
}
Claims claims = TokenUtil.parseJWT(token);
String userName = (String) claims.get("userName");
String resListIsNotAdmin = (String) claims.get("resList");
//擷取角色有的資源
String[] arr = resListIsNotAdmin != null && StringUtils.isNotEmpty(resListIsNotAdmin) ? resListIsNotAdmin.split(",") : null;
if(arr == null || arr.length == 0){
return simpleAuthorizationInfo;
}
for(String res : arr){
simpleAuthorizationInfo.addStringPermission(res);
}
return simpleAuthorizationInfo;
}
//使用者認證
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
//加這一步的目的是在Post請求的時候會先進認證,然後在到請求
String token = (String) auth.getPrincipal();
if (token == null) {
return null;
}
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(token, token, getName());
return simpleAuthenticationInfo;
}
}
七:接下來是登入和操作案例
1.控制層
/**
* 使用者登入
* [userName, passWord]
* @return {@link Result}
* @throws
* @author 李慶偉
* @date 2020/4/26 15:19
*/
@PostMapping(value = "login",name = "user/login")
@ResponseBody
public Result login(String userName, String passWord){
String tokenId = userService.login(userName,passWord);
return Result.success(tokenId);
}
/**
* 添加使用者
* [userName, passWord]
* @return {@link Result}
* @throws
* @author 李慶偉
* @date 2020/4/26 15:19
*/
@RequiresPermissions("user:add")
@PostMapping("add")
@ResponseBody
public Result add(@RequestParam(value = "userName", required = true)String userName,
@RequestParam(value = "nickName", required = true)String nickName,
@RequestParam(value = "tele", required = true)String tele){
User user = userService.add(userName,nickName,tele);
return Result.success(user);
}
2.接口
/**
* 登入
* [userName]
* @return {@link User}
* @throws
* @author 李慶偉
* @date 2020/4/23 13:36
*/
String login(String userName, String passWord);
/**
* 使用者添加
* [userName, passWord]
* @return {@link User}
* @throws
* @author 李慶偉
* @date 2020/4/26 15:10
*/
User add(String userName, String nickName, String tele);
3.接口實作類
/**
* 使用者登入
* [userName, passWord]
* @return {@link User}
* @throws
* @author 李慶偉
* @date 2020/4/23 13:37
*/
public String login(String userName, String passWord) {
if(StringUtils.isEmpty(userName) || StringUtils.isEmpty(passWord)){
throw new MyException(ResultEnum.USER_LOGIN_ERROR.getExpKey(), ResultEnum.USER_LOGIN_ERROR.getExpValue());
}
QueryWrapper wrapper = new QueryWrapper();
wrapper.eq("user_name",userName);
wrapper.eq("pass_word", Md5Util.md5(passWord));
List<User> list = userMapper.selectList(wrapper);
if((list == null || list.size() != 1 ) && !userName.equals("admin")){
throw new MyException(ResultEnum.USER_LOGIN_ERROR.getExpKey(), ResultEnum.USER_LOGIN_ERROR.getExpValue());
}
User user = new User();
//如果是管理者有全部權限
if(userName.equals("admin") && passWord.equals("admin")){
user.setId("admin");
user.setNickName("我是管理者");
user.setUserName("admin");
user.setPassWord(Md5Util.md5("admin"));
user.setTele("66666666666");
//特殊邏輯,管理者應該有所有權限,這裡暫時沒有寫,隻模拟了非管理者的情況
} else {
user = list.get(0);
List<Role> roleList = new ArrayList<Role>();
Role role = new Role();
role.setId("1");
role.setRoleName("我是超級管理者");
roleList.add(role);
user.setRoleList(roleList);
List<Res> resList = new ArrayList<Res>();
Res res1 = new Res();
res1.setId("11");
res1.setResCode("user:show");
resList.add(res1);
Res res2 = new Res();
res2.setId("12");
res2.setResCode("user:add");
resList.add(res2);
user.setResList(resList);
}
Subject subject = SecurityUtils.getSubject();
String token = TokenUtil.createJWT(user);
JwtToken jwtToken = new JwtToken(token);
subject.login(jwtToken);
return token;
}
/**
* 使用者添加
* [userName, passWord]
* @return {@link User}
* @throws
* @author 李慶偉
* @date 2020/4/26 15:11
*/
public User add(String userName, String nickName, String tele) {
//添加使用者前,判斷使用者名是否重複
Map<String,Object> map = new HashMap<String,Object>();
map.put("user_name",userName);
if(StringUtils.isNotEmpty(userName) && userName.equals("admin")){
throw new MyException(ResultEnum.USER_ADD_REPEAT.getExpKey(), ResultEnum.USER_ADD_REPEAT.getExpValue());
}
List<User> list = userMapper.selectByMap(map);
if(list != null && list.size() > 0){
throw new MyException(ResultEnum.USER_ADD_REPEAT.getExpKey(), ResultEnum.USER_ADD_REPEAT.getExpValue());
}
User user = new User();
user.setUserName(userName);
user.setPassWord(Md5Util.md5("1"));
user.setNickName(nickName);
user.setTele(tele);
user.setCreateTime(new Date());
userMapper.insert(user);
return user;
}
到此文章結束。。。。。。。。。