1. 程式人生 > 其它 >springboot shiro自定義攔截器結合token實現登入狀態和許可權認證

springboot shiro自定義攔截器結合token實現登入狀態和許可權認證

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