Shiro基于SpringBoot +JWT搭建简单的restful服务

Overview

Shiro + JWT + Spring Boot Restful 简易教程

GitHub 项目地址:https://github.com/Smith-Cruise/Spring-Boot-Shiro

序言

我也是半路出家的人,如果大家有什么好的意见或批评,请务必 issue 下。

如果想要直接体验,直接 clone 项目,运行 mvn spring-boot:run 命令即可进行访问。网址规则自行看教程后面。

如果想了解 Spring Security 可以看

Spring Boot 2.0+Srping Security+Thymeleaf的简易教程

Spring Boot 2 + Spring Security 5 + JWT 的单页应用Restful解决方案 (推荐)

特性

  • 完全使用了 Shiro 的注解配置,保持高度的灵活性。
  • 放弃 Cookie ,Session ,使用JWT进行鉴权,完全实现无状态鉴权。
  • JWT 密钥支持过期时间。
  • 对跨域提供支持。

准备工作

在开始本教程之前,请保证已经熟悉以下几点。

  • Spring Boot 基本语法,至少要懂得 ControllerRestControllerAutowired 等这些基本注释。其实看看官方的 Getting-Start 教程就差不多了。
  • JWT (Json Web Token)的基本概念,并且会简单操作JWT的 JAVA SDK
  • Shiro 的基本操作,看下官方的 10 Minute Tutorial 即可。
  • 模拟 HTTP 请求工具,我使用的是 PostMan。

简要的说明下我们为什么要用 JWT ,因为我们要实现完全的前后端分离,所以不可能使用 sessioncookie 的方式进行鉴权,所以 JWT 就被派上了用场,你可以通过一个加密密钥来进行前后端的鉴权。

程序逻辑

  1. 我们 POST 用户名与密码到 /login 进行登入,如果成功返回一个加密 token,失败的话直接返回 401 错误。
  2. 之后用户访问每一个需要权限的网址请求必须在 header 中添加 Authorization 字段,例如 Authorization: tokentoken 为密钥。
  3. 后台会进行 token 的校验,如果有误会直接返回 401。

Token加密说明

  • 携带了 username 信息在 token 中。
  • 设定了过期时间。
  • 使用用户登入密码对 token 进行加密。

Token校验流程

  1. 获得 token 中携带的 username 信息。
  2. 进入数据库搜索这个用户,得到他的密码。
  3. 使用用户的密码来检验 token 是否正确。

准备Maven文件

新建一个 Maven 工程,添加相关的 dependencies。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.inlighting</groupId>
    <artifactId>shiro-study</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>

        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.3.2</version>
        </dependency>
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.2.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>1.5.8.RELEASE</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
        		<!-- Srping Boot 打包工具 -->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>1.5.7.RELEASE</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <!-- 指定JDK编译版本 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

注意指定JDK版本和编码。

构建简易的数据源

为了缩减教程的代码,我使用 HashMap 本地模拟了一个数据库,结构如下:

username password role permission
smith smith123 user view
danny danny123 admin view,edit

这是一个最简单的用户权限表,如果想更加进一步了解,自行百度 RBAC。

之后再构建一个 UserService 来模拟数据库查询,并且把结果放到 UserBean 之中。

UserService.java

@Component
public class UserService {

    public UserBean getUser(String username) {
        // 没有此用户直接返回null
        if (! DataSource.getData().containsKey(username))
            return null;

        UserBean user = new UserBean();
        Map<String, String> detail = DataSource.getData().get(username);

        user.setUsername(username);
        user.setPassword(detail.get("password"));
        user.setRole(detail.get("role"));
        user.setPermission(detail.get("permission"));
        return user;
    }
}

UserBean.java

public class UserBean {
    private String username;

    private String password;

    private String role;

    private String permission;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getRole() {
        return role;
    }

    public void setRole(String role) {
        this.role = role;
    }

    public String getPermission() {
        return permission;
    }

    public void setPermission(String permission) {
        this.permission = permission;
    }
}

配置 JWT

我们写一个简单的 JWT 加密,校验工具,并且使用用户自己的密码充当加密密钥,这样保证了 token 即使被他人截获也无法破解。并且我们在 token 中附带了 username 信息,并且设置密钥5分钟就会过期。

public class JWTUtil {

    // 过期时间5分钟
    private static final long EXPIRE_TIME = 5*60*1000;

    /**
     * 校验token是否正确
     * @param token 密钥
     * @param secret 用户的密码
     * @return 是否正确
     */
    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 exception) {
            return false;
        }
    }

    /**
     * 获得token中的信息无需secret解密也能获得
     * @return token中包含的用户名
     */
    public static String getUsername(String token) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim("username").asString();
        } catch (JWTDecodeException e) {
            return null;
        }
    }

    /**
     * 生成签名,5min后过期
     * @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 (UnsupportedEncodingException e) {
            return null;
        }
    }
}

构建URL

ResponseBean.java

既然想要实现 restful,那我们要保证每次返回的格式都是相同的,因此我建立了一个 ResponseBean 来统一返回的格式。

public class ResponseBean {
    
    // http 状态码
    private int code;

    // 返回信息
    private String msg;

    // 返回的数据
    private Object data;

    public ResponseBean(int code, String msg, Object data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }
}

自定义异常

为了实现我自己能够手动抛出异常,我自己写了一个 UnauthorizedException.java

public class UnauthorizedException extends RuntimeException {
    public UnauthorizedException(String msg) {
        super(msg);
    }

    public UnauthorizedException() {
        super();
    }
}

URL结构

URL 作用
/login 登入
/article 所有人都可以访问,但是用户与游客看到的内容不同
/require_auth 登入的用户才可以进行访问
/require_role admin的角色用户才可以登入
/require_permission 拥有view和edit权限的用户才可以访问

Controller

@RestController
public class WebController {

    private static final Logger LOGGER = LogManager.getLogger(WebController.class);

    private UserService userService;

    @Autowired
    public void setService(UserService userService) {
        this.userService = userService;
    }

    @PostMapping("/login")
    public ResponseBean login(@RequestParam("username") String username,
                              @RequestParam("password") String password) {
        UserBean userBean = userService.getUser(username);
        if (userBean.getPassword().equals(password)) {
            return new ResponseBean(200, "Login success", JWTUtil.sign(username, password));
        } else {
            throw new UnauthorizedException();
        }
    }

    @GetMapping("/article")
    public ResponseBean article() {
        Subject subject = SecurityUtils.getSubject();
        if (subject.isAuthenticated()) {
            return new ResponseBean(200, "You are already logged in", null);
        } else {
            return new ResponseBean(200, "You are guest", null);
        }
    }

    @GetMapping("/require_auth")
    @RequiresAuthentication
    public ResponseBean requireAuth() {
        return new ResponseBean(200, "You are authenticated", null);
    }

    @GetMapping("/require_role")
    @RequiresRoles("admin")
    public ResponseBean requireRole() {
        return new ResponseBean(200, "You are visiting require_role", null);
    }

    @GetMapping("/require_permission")
    @RequiresPermissions(logical = Logical.AND, value = {"view", "edit"})
    public ResponseBean requirePermission() {
        return new ResponseBean(200, "You are visiting permission require edit,view", null);
    }

    @RequestMapping(path = "/401")
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    public ResponseBean unauthorized() {
        return new ResponseBean(401, "Unauthorized", null);
    }
}

处理框架异常

之前说过 restful 要统一返回的格式,所以我们也要全局处理 Spring Boot 的抛出异常。利用 @RestControllerAdvice 能很好的实现。

@RestControllerAdvice
public class ExceptionController {

    // 捕捉shiro的异常
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    @ExceptionHandler(ShiroException.class)
    public ResponseBean handle401(ShiroException e) {
        return new ResponseBean(401, e.getMessage(), null);
    }

    // 捕捉UnauthorizedException
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    @ExceptionHandler(UnauthorizedException.class)
    public ResponseBean handle401() {
        return new ResponseBean(401, "Unauthorized", null);
    }

    // 捕捉其他所有异常
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ResponseBean globalException(HttpServletRequest request, Throwable ex) {
        return new ResponseBean(getStatus(request).value(), ex.getMessage(), null);
    }

    private HttpStatus getStatus(HttpServletRequest request) {
        Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");
        if (statusCode == null) {
            return HttpStatus.INTERNAL_SERVER_ERROR;
        }
        return HttpStatus.valueOf(statusCode);
    }
}

配置 Shiro

大家可以先看下官方的 Spring-Shiro 整合教程,有个初步的了解。不过既然我们用了 Spring-Boot,那我们肯定要争取零配置文件。

实现JWTToken

JWTToken 差不多就是 Shiro 用户名密码的载体。因为我们是前后端分离,服务器无需保存用户状态,所以不需要 RememberMe 这类功能,我们简单的实现下 AuthenticationToken 接口即可。因为 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;
    }
}

实现Realm

realm 的用于处理用户是否合法的这一块,需要我们自己实现。

@Service
public class MyRealm extends AuthorizingRealm {

    private static final Logger LOGGER = LogManager.getLogger(MyRealm.class);

    private UserService userService;

    @Autowired
    public void setUserService(UserService userService) {
        this.userService = userService;
    }

    /**
     * 大坑!,必须重写此方法,不然Shiro会报错
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JWTToken;
    }

    /**
     * 只有当需要检测用户权限的时候才会调用此方法,例如checkRole,checkPermission之类的
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String username = JWTUtil.getUsername(principals.toString());
        UserBean user = userService.getUser(username);
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        simpleAuthorizationInfo.addRole(user.getRole());
        Set<String> permission = new HashSet<>(Arrays.asList(user.getPermission().split(",")));
        simpleAuthorizationInfo.addStringPermissions(permission);
        return simpleAuthorizationInfo;
    }

    /**
     * 默认使用此方法进行用户名正确与否验证,错误抛出异常即可。
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
        String token = (String) auth.getCredentials();
        // 解密获得username,用于和数据库进行对比
        String username = JWTUtil.getUsername(token);
        if (username == null) {
            throw new AuthenticationException("token invalid");
        }

        UserBean userBean = userService.getUser(username);
        if (userBean == null) {
            throw new AuthenticationException("User didn't existed!");
        }

        if (! JWTUtil.verify(token, username, userBean.getPassword())) {
            throw new AuthenticationException("Username or password error");
        }

        return new SimpleAuthenticationInfo(token, token, "my_realm");
    }
}

doGetAuthenticationInfo() 中用户可以自定义抛出很多异常,详情见文档。

重写 Filter

所有的请求都会先经过 Filter,所以我们继承官方的 BasicHttpAuthenticationFilter ,并且重写鉴权的方法。

代码的执行流程 preHandle -> isAccessAllowed -> isLoginAttempt -> executeLogin

public class JWTFilter extends BasicHttpAuthenticationFilter {

    private Logger LOGGER = LoggerFactory.getLogger(this.getClass());

    /**
     * 判断用户是否想要登入。
     * 检测header里面是否包含Authorization字段即可
     */
    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
        HttpServletRequest req = (HttpServletRequest) request;
        String authorization = req.getHeader("Authorization");
        return authorization != null;
    }

    /**
     *
     */
    @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;
    }

    /**
     * 这里我们详细说明下为什么最终返回的都是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) {
            LOGGER.error(e.getMessage());
        }
    }
}

getSubject(request, response).login(token); 这一步就是提交给了 realm 进行处理。

配置Shiro

@Configuration
public class ShiroConfig {

    @Bean("securityManager")
    public DefaultWebSecurityManager getManager(MyRealm realm) {
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        // 使用自己的realm
        manager.setRealm(realm);

        /*
         * 关闭shiro自带的session,详情见文档
         * http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29
         */
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        manager.setSubjectDAO(subjectDAO);

        return manager;
    }

    @Bean("shiroFilter")
    public ShiroFilterFactoryBean factory(DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();

        // 添加自己的过滤器并且取名为jwt
        Map<String, Filter> filterMap = new HashMap<>();
        filterMap.put("jwt", new JWTFilter());
        factoryBean.setFilters(filterMap);

        factoryBean.setSecurityManager(securityManager);
        factoryBean.setUnauthorizedUrl("/401");

        /*
         * 自定义url规则
         * http://shiro.apache.org/web.html#urls-
         */
        Map<String, String> filterRuleMap = new HashMap<>();
        // 所有请求通过我们自己的JWT Filter
        filterRuleMap.put("/**", "jwt");
        // 访问401和404页面不通过我们的Filter
        filterRuleMap.put("/401", "anon");
        factoryBean.setFilterChainDefinitionMap(filterRuleMap);
        return factoryBean;
    }

    /**
     * 下面的代码是添加注解支持
     */
    @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;
    }
}

里面 URL 规则自己参考文档即可 http://shiro.apache.org/web.html

总结

我就说下代码还有哪些可以进步的地方吧

  • 没有实现 Shiro 的 Cache 功能。
  • Shiro 中鉴权失败时不能够直接返回 401 信息,而是通过跳转到 /401 地址实现。
Comments
  • 关于在realm进行登入时抛出异常的问题

    关于在realm进行登入时抛出异常的问题

    在JWTFilter中有如下方法

    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String authorization = httpServletRequest.getHeader("Authorization");
        authorization = authorization.replace("Bearer ", "");
    
        JWTToken token = new JWTToken(authorization);
        // 提交给realm进行登入,如果错误他会抛出异常并被捕获
        getSubject(request, response).login(token);
         // 如果没有抛出异常则代表登入成功,返回true
        return true;
     }
    

    此时Header中带有Authorization,那么将会去MyRealm中进行用户验证。

    如果Authorization传过来的token是错误的,就会抛出异常。但是再MyRealm.java 中的doGetAuthenticationInfo中抛出的异常,无法被ExceptionController所捕获,就导致返回的数据结构不统一了。 请问有解决办法吗?

    opened by whyalwaysmea 9
  • 没跑起来

    没跑起来

    能看出来作者确实用心在写文档,但我能力有限跑起来有问题 hashmap怎么模拟的? { "username": "smith", "password": "smith123", "role": "user", "permission": "view" } 返回: { "code": 500, "msg": "Required String parameter 'username' is not present", "data": null }

    opened by prlei 8
  • 关于使用前后分离,后端类RESTful风格改造配置Shiro的一些问题和想法

    关于使用前后分离,后端类RESTful风格改造配置Shiro的一些问题和想法

    关于项目本身:

    1. 个人理解shiro的cache主要用在session方面,既然使用了JWT鉴权,似乎就没必要关心shiro的cache机制,至于验证鉴权以外其他方面的缓存需,完全可以直接使用 spring-boot-starter-cache, 结合其他cache实现来做.

    2.jwt 串放在Header的Authorization字段上的时候一般开头是Bearer,隔一个空格后才是jwt串,这样区别于BasicDigest

    opened by minghu6 7
  • 权限控制的方法

    权限控制的方法

    在控制器的其中一个方法加了权限判断注解,按我的理解是这个方法没有权限,访问就会受限,但是实际上这个控制器的其它方法也受到了同样的限制。请问这里的逻辑是怎样的 代码如下

    @RestController
    @RequestMapping("/users")
    public class UserController {
        @Autowired
        private UserRepository userRepository;
    
        @Autowired
        private UserService userService;
    
        @GetMapping
    //    @RequiresPermissions("user:index")
        public ResponseContent index() {
            return ResponseUtil.success(userRepository.findAll());
        }
    
        @PostMapping
        @RequiresPermissions("user:store")
        public ResponseContent store(@RequestParam String username, @RequestParam String password) {
            User user = userService.createNewUser(username, password);
    
            return ResponseUtil.success(user);
        }
    
        @DeleteMapping("/{id}")
    //    @RequiresPermissions("user:del")
        public ResponseContent delete(@PathVariable long id) {
            userRepository.delete(id);
            return ResponseUtil.success();
        }
    }
    

    访问get http://localhost:8080/users会得到:

    {
        "timestamp": 1516766046036,
        "status": 404,
        "error": "Not Found",
        "message": "No message available",
        "path": "/users"
    }
    
    opened by ba6yface 4
  • DELETE方法跨域问题

    DELETE方法跨域问题

    你好,有一个问题,preHandle(ServletRequest request, ServletResponse response)方法按照你的方式写好之后,执行Spring MVC 的@DeleteMapping的方法会报错 Method DELETE is not allowed by Access-Control-Allow-Methods in preflight response. 但是如果我将这一句 httpResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, httpRequest.getMethod()); 修改为下面的样子的话,就可以正常执行 httpResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, httpRequest.getMethod()+", DELETE");

    但我不太清楚为什么

    opened by houzw 3
  • 在此工程中pom文件引入spring-boot-starter-parent后起冲突的问题

    在此工程中pom文件引入spring-boot-starter-parent后起冲突的问题

    在pom文件中加入以下依赖

    <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>1.5.8.RELEASE</version>
            <relativePath/> <!-- lookup parent from repository -->
    </parent>
    
    <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.44</version>
    </dependency>
    

    返回全部变成404

    {
        "timestamp": 1519257821400,
        "status": 404,
        "error": "Not Found",
        "message": "No message available",
        "path": "/require_role"
    }
    

    后来发现把

    <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>1.5.8.RELEASE</version>
            <relativePath/> <!-- lookup parent from repository -->
    </parent>
    

    去掉就会正常。

    或者保留spring-boot-starter-parent,把spring-boot-starter-data-jpa去掉也会正常,但不知道为什么会这样???

    opened by MissThee 3
  • Bump shiro-spring from 1.3.2 to 1.7.0

    Bump shiro-spring from 1.3.2 to 1.7.0

    Bumps shiro-spring from 1.3.2 to 1.7.0.

    Dependabot compatibility score

    Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


    Dependabot commands and options

    You can trigger Dependabot actions by commenting on this PR:

    • @dependabot rebase will rebase this PR
    • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
    • @dependabot merge will merge this PR after your CI passes on it
    • @dependabot squash and merge will squash and merge this PR after your CI passes on it
    • @dependabot cancel merge will cancel a previously requested merge and block automerging
    • @dependabot reopen will reopen this PR if it is closed
    • @dependabot close will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
    • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
    • @dependabot use these labels will set the current labels as the default for future PRs for this repo and language
    • @dependabot use these reviewers will set the current reviewers as the default for future PRs for this repo and language
    • @dependabot use these assignees will set the current assignees as the default for future PRs for this repo and language
    • @dependabot use this milestone will set the current milestone as the default for future PRs for this repo and language

    You can disable automated security fix PRs for this repo from the Security Alerts page.

    dependencies 
    opened by dependabot[bot] 2
  • 关于url拦截

    关于url拦截

    关闭shiro自带的session,这个目的应该是为了每次请求都重新认证一次,我用post验证过,发现是因为无JSESSIONID返回,所以每次都会执行 doGetAuthenticationInfo方法认证,这是合理的。 但是我发现如果不用注解,使用url拦截的话,代码如下,又会重新返回JSESSIONID,并且只会执行一次认证,后续每次都执行doGetAuthorizationInfo方法授权 ` @Bean("shiroFilter") public ShiroFilterFactoryBean factory(DefaultWebSecurityManager securityManager) { ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();

        // 添加自己的过滤器并且取名为jwt
        Map<String, Filter> filterMap = new HashMap<>();
        filterMap.put("jwt", new JWTFilter());
        factoryBean.setFilters(filterMap);
    
        factoryBean.setSecurityManager(securityManager);
        factoryBean.setUnauthorizedUrl("/401");
    
        /*
         * 自定义url规则
         * http://shiro.apache.org/web.html#urls-
         */
        Map<String, String> filterRuleMap = new HashMap<>();
        filterRuleMap.put("/require_role","roles[admin]");
        // 所有请求通过我们自己的JWT Filter
        filterRuleMap.put("/**", "jwt");
        // 访问401和404页面不通过我们的Filter
        filterRuleMap.put("/401", "anon");
        factoryBean.setFilterChainDefinitionMap(filterRuleMap);
        return factoryBean;
    }`
    
    opened by WillLiaowh 2
  • Bump spring-boot-starter-web from 1.5.8.RELEASE to 2.5.12

    Bump spring-boot-starter-web from 1.5.8.RELEASE to 2.5.12

    Bumps spring-boot-starter-web from 1.5.8.RELEASE to 2.5.12.

    Release notes

    Sourced from spring-boot-starter-web's releases.

    v2.5.12

    :lady_beetle: Bug Fixes

    • MustacheAutoConfiguration in a Servlet web application fails with a ClassNotFoundException when Spring MVC is not on the classpath #30456

    :notebook_with_decorative_cover: Documentation

    • Javadoc of org.springframework.boot.gradle.plugin.ResolveMainClassName.setClasspath(Object) is inaccurate #30468
    • Document that @DefaultValue can be used on a record component #30460

    :hammer: Dependency Upgrades

    • Upgrade to Jackson Bom 2.12.6.20220326 #30477
    • Upgrade to Spring Framework 5.3.18 #30491

    :heart: Contributors

    We'd like to thank all the contributors who worked on this release!

    v2.5.11

    :star: New Features

    • Add EIGHTEEN to JavaVersion enum #29524

    :lady_beetle: Bug Fixes

    • Thymeleaf auto-configuration in a reactive application can fail due to duplicate templateEngine beans #30384
    • ConfigurationPropertyName#equals is not symmetric when adapt has removed trailing characters from an element #30317
    • server.tomcat.keep-alive-timeout is not applied to HTTP/2 #30267
    • Setting spring.mustache.enabled to false has no effect #30250
    • bootWar is configured eagerly #30211
    • Actuator @ReadOperation on Flux cancels request after first element emitted #30095
    • No metrics are bound for R2DBC ConnectionPools that have been wrapped #30090
    • Unnecessary allocations in Prometheus scraping endpoint #30085
    • Condition evaluation report entry for a @ConditionalOnSingleCandidate that does not match due to multiple primary beans isn't as clear as it could be #30073
    • Generated password are logged without an "unsuitable for production use" note #30061
    • Files in META-INF are not found when deploying a Gradle-built executable war to a servlet container #30026
    • spring-boot-configuration-processor fails compilation due to @DefaultValue with a long value and generates invalid metadata for byte and short properties with out-of-range default values #30020
    • Dependency management for Netty tcNative is incomplete leading to possible version conflicts #30010
    • Dependency management for Apache Kafka is incomplete #29023

    :notebook_with_decorative_cover: Documentation

    • Fix JsonSerializer example in reference guide #30329
    • Default value of spring.thymeleaf.reactive.media-types is not documented #30280
    • Add Netty in "Enable HTTP Response Compression" #30234

    ... (truncated)

    Commits
    • 35105a0 Release v2.5.12
    • 17936b8 Polish
    • 94c40c7 Upgrade to Spring Framework 5.3.18
    • 2e90fd2 Upgrade CI to Docker 20.10.14
    • 6cded5b Upgrade Java 18 version in CI image
    • 06c5e26 Upgrade to Jackson Bom 2.12.6.20220326
    • c0c32d8 Merge pull request #30456 from candrews
    • 8cb11b7 Polish "Make MustacheViewResolver bean back off without Spring MVC"
    • 7101b50 Make MustacheViewResolver bean back off without Spring MVC
    • 05b7bef Fix javadoc of ResolveMainClassName setClasspath(Object)
    • Additional commits viewable in compare view

    Dependabot compatibility score

    Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


    Dependabot commands and options

    You can trigger Dependabot actions by commenting on this PR:

    • @dependabot rebase will rebase this PR
    • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
    • @dependabot merge will merge this PR after your CI passes on it
    • @dependabot squash and merge will squash and merge this PR after your CI passes on it
    • @dependabot cancel merge will cancel a previously requested merge and block automerging
    • @dependabot reopen will reopen this PR if it is closed
    • @dependabot close will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
    • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
    • @dependabot use these labels will set the current labels as the default for future PRs for this repo and language
    • @dependabot use these reviewers will set the current reviewers as the default for future PRs for this repo and language
    • @dependabot use these assignees will set the current assignees as the default for future PRs for this repo and language
    • @dependabot use this milestone will set the current milestone as the default for future PRs for this repo and language

    You can disable automated security fix PRs for this repo from the Security Alerts page.

    dependencies 
    opened by dependabot[bot] 1
  • Bump shiro-spring from 1.3.2 to 1.7.1

    Bump shiro-spring from 1.3.2 to 1.7.1

    Bumps shiro-spring from 1.3.2 to 1.7.1.

    Dependabot compatibility score

    Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


    Dependabot commands and options

    You can trigger Dependabot actions by commenting on this PR:

    • @dependabot rebase will rebase this PR
    • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
    • @dependabot merge will merge this PR after your CI passes on it
    • @dependabot squash and merge will squash and merge this PR after your CI passes on it
    • @dependabot cancel merge will cancel a previously requested merge and block automerging
    • @dependabot reopen will reopen this PR if it is closed
    • @dependabot close will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
    • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
    • @dependabot use these labels will set the current labels as the default for future PRs for this repo and language
    • @dependabot use these reviewers will set the current reviewers as the default for future PRs for this repo and language
    • @dependabot use these assignees will set the current assignees as the default for future PRs for this repo and language
    • @dependabot use this milestone will set the current milestone as the default for future PRs for this repo and language

    You can disable automated security fix PRs for this repo from the Security Alerts page.

    dependencies 
    opened by dependabot[bot] 0
  • Token 令牌建議不要存放密碼

    Token 令牌建議不要存放密碼

    我是去了Udemy看了別人課程security+jwt 再來這邊造訪 因為剛好再需要整合多一層 shiro 也很感謝大大願意分享你的編寫思路 並略為修改了大大的JWTUtil部份 將 jwtSecret 取代為密碼 並保存在 Spring resources application 並再加入多一次驗證 username是否與token內的username一樣 而jwtExpirationInMs 也是保存在Spring resources application 方便後續修改

    public static boolean verify(String token, String username) { try { Algorithm algorithm = Algorithm.HMAC256(jwtSecret); JWTVerifier verifier = JWT.require(algorithm) .withClaim("username", username) .build(); DecodedJWT decodeJWT = verifier.verify(token);

        // verify username
        String username_in_token = decodeJWT.getClaim("username").asString();
    
        if (! username_in_token.equals(username)) {
            throw new APIException("username doesn't match token", HttpStatus.UNAUTHORIZED);
        }
    
        // verification passed
        return true;
    
    }  catch (TokenExpiredException e) {
        throw new APIException("token is expired", HttpStatus.UNAUTHORIZED);
    
    }   catch (Exception exception) {
        throw new APIException("unknown exception has been raised", HttpStatus.UNAUTHORIZED);
    }
    

    }

    public static String sign(String username) { try { Date current_date = new Date(System.currentTimeMillis()); Date expire_date = new Date(System.currentTimeMillis() + jwtExpirationInMs); Algorithm algorithm = Algorithm.HMAC256(jwtSecret);

        String token = JWT.create()
                .withClaim("username", username)
                .withIssuedAt(current_date) // Assign Datetime
                .withExpiresAt(expire_date)  // Expire Datetime
                .withClaim("username", username) // Attach username to verify
                .sign(algorithm);
    
        return token;
    
    } catch (UnsupportedEncodingException e) {
        return null;
    }
    

    }

    opened by goseesomething 1
  • Dependency org.apache.shiro:shiro-web, leading to CVE problem

    Dependency org.apache.shiro:shiro-web, leading to CVE problem

    Hi, In Spring-Boot-Shiro,there is a dependency org.apache.shiro:shiro-web:1.3.2 that calls the risk method.

    CVE-2020-11989

    The scope of this CVE affected version is [,1.6.0)

    After further analysis, in this project, the main Api called is <org.apache.shiro.web.util.WebUtils: java.lang.String getPathWithinApplication(javax.servlet.http.HttpServletRequest)>

    Risk method repair link : GitHub

    CVE Bug Invocation Path--

    Path Length : 5

    <org.apache.shiro.web.util.WebUtils: java.lang.String getPathWithinApplication(javax.servlet.http.HttpServletRequest)>
    at <org.apache.shiro.web.filter.PathMatchingFilter: java.lang.String getPathWithinApplication(javax.servlet.ServletRequest)> (org.apache.shiro.web.filter.PathMatchingFilter.java:[103]) in /.m2/repository/org/apache/shiro/shiro-web/1.3.2/shiro-web-1.3.2.jar
    at <org.apache.shiro.web.filter.PathMatchingFilter: boolean pathsMatch(java.lang.String,javax.servlet.ServletRequest)> (org.apache.shiro.web.filter.PathMatchingFilter.java:[122]) in /.m2/repository/org/apache/shiro/shiro-web/1.3.2/shiro-web-1.3.2.jar
    at <org.apache.shiro.web.filter.PathMatchingFilter: boolean preHandle(javax.servlet.ServletRequest,javax.servlet.ServletResponse)> (org.apache.shiro.web.filter.PathMatchingFilter.java:[175]) in /.m2/repository/org/apache/shiro/shiro-web/1.3.2/shiro-web-1.3.2.jar
    at <org.inlighting.shiro.JWTFilter: boolean preHandle(javax.servlet.ServletRequest,javax.servlet.ServletResponse)> (org.inlighting.shiro.JWTFilter.java:[81]) in /detect/unzip/Spring-Boot-Shiro-master/target/classesc
    
    

    Dependency tree--

    [INFO] org.inlighting:shiro-study:jar:1.1
    [INFO] +- org.slf4j:slf4j-api:jar:1.7.16:compile
    [INFO] +- org.apache.shiro:shiro-spring:jar:1.3.2:compile
    [INFO] |  +- org.apache.shiro:shiro-core:jar:1.3.2:compile
    [INFO] |  |  \- commons-beanutils:commons-beanutils:jar:1.8.3:compile
    [INFO] |  \- org.apache.shiro:shiro-web:jar:1.3.2:compile
    [INFO] +- com.auth0:java-jwt:jar:3.2.0:compile
    [INFO] |  +- com.fasterxml.jackson.core:jackson-databind:jar:2.8.4:compile
    [INFO] |  |  +- com.fasterxml.jackson.core:jackson-annotations:jar:2.8.0:compile
    [INFO] |  |  \- com.fasterxml.jackson.core:jackson-core:jar:2.8.4:compile
    [INFO] |  +- commons-codec:commons-codec:jar:1.10:compile
    [INFO] |  \- org.bouncycastle:bcprov-jdk15on:jar:1.55:compile
    [INFO] \- org.springframework.boot:spring-boot-starter-web:jar:1.5.8.RELEASE:compile
    [INFO]    +- org.springframework.boot:spring-boot-starter:jar:1.5.8.RELEASE:compile
    [INFO]    |  +- org.springframework.boot:spring-boot:jar:1.5.8.RELEASE:compile
    [INFO]    |  +- org.springframework.boot:spring-boot-autoconfigure:jar:1.5.8.RELEASE:compile
    [INFO]    |  +- org.springframework.boot:spring-boot-starter-logging:jar:1.5.8.RELEASE:compile
    [INFO]    |  |  +- ch.qos.logback:logback-classic:jar:1.1.11:compile
    [INFO]    |  |  |  \- ch.qos.logback:logback-core:jar:1.1.11:compile
    [INFO]    |  |  +- org.slf4j:jcl-over-slf4j:jar:1.7.25:compile
    [INFO]    |  |  +- org.slf4j:jul-to-slf4j:jar:1.7.25:compile
    [INFO]    |  |  \- org.slf4j:log4j-over-slf4j:jar:1.7.25:compile
    [INFO]    |  +- org.springframework:spring-core:jar:4.3.12.RELEASE:compile
    [INFO]    |  \- org.yaml:snakeyaml:jar:1.17:runtime
    [INFO]    +- org.springframework.boot:spring-boot-starter-tomcat:jar:1.5.8.RELEASE:compile
    [INFO]    |  +- org.apache.tomcat.embed:tomcat-embed-core:jar:8.5.23:compile
    [INFO]    |  |  \- org.apache.tomcat:tomcat-annotations-api:jar:8.5.23:compile
    [INFO]    |  +- org.apache.tomcat.embed:tomcat-embed-el:jar:8.5.23:compile
    [INFO]    |  \- org.apache.tomcat.embed:tomcat-embed-websocket:jar:8.5.23:compile
    [INFO]    +- org.hibernate:hibernate-validator:jar:5.3.5.Final:compile
    [INFO]    |  +- javax.validation:validation-api:jar:1.1.0.Final:compile
    [INFO]    |  +- org.jboss.logging:jboss-logging:jar:3.3.0.Final:compile
    [INFO]    |  \- com.fasterxml:classmate:jar:1.3.1:compile
    [INFO]    +- org.springframework:spring-web:jar:4.3.12.RELEASE:compile
    [INFO]    |  +- org.springframework:spring-aop:jar:4.3.12.RELEASE:compile
    [INFO]    |  +- org.springframework:spring-beans:jar:4.3.12.RELEASE:compile
    [INFO]    |  \- org.springframework:spring-context:jar:4.3.12.RELEASE:compile
    [INFO]    \- org.springframework:spring-webmvc:jar:4.3.12.RELEASE:compile
    [INFO]       \- org.springframework:spring-expression:jar:4.3.12.RELEASE:compile
    

    Suggested solutions:

    Update dependency version

    Thank you very much.

    opened by CVEDetect 1
Owner
Smith Cruise
With great power comes great responsibility.
Smith Cruise