天天看點

springboot+shiro(自定義攔截器)+jwt實作前後端分離權限管理

說明:前後端不分離的時候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;
}
           

到此文章結束。。。。。。。。。