GithubHelp home page GithubHelp logo

spring-boot-shiro's Introduction

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 地址实现。

spring-boot-shiro's People

Contributors

ica10888 avatar smith-cruise avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

spring-boot-shiro's Issues

发现个问题

Shiro源码是执行isAccessAllowed和onAccessDenied方法,现在只重写了isAccessAllowed,而onAccessDenied源码是重复调用了executeLogin方法,造成了循环多次调用Shiro认证方法

关闭 Session 的简单方法

在自定义 FilteronPreHandle 方法中添加如下代码即可。

request.setAttribute(DefaultSubjectContext.SESSION_CREATION_ENABLED, Boolean.FALSE);

对于不需要认证的页面使用 NoSessionCreationFilter 而不是 anon

filterRuleMap.put("/401", "noSessionCreation");

具体可看官方文档 NoSessionCreationFilter 和源码

package org.apache.shiro.web.filter.session;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import org.apache.shiro.subject.support.DefaultSubjectContext;
import org.apache.shiro.web.filter.PathMatchingFilter;

public class NoSessionCreationFilter extends PathMatchingFilter {
    public NoSessionCreationFilter() {
    }

    protected boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
        request.setAttribute(DefaultSubjectContext.SESSION_CREATION_ENABLED, Boolean.FALSE);
        return true;
    }
}

请问如何在Controller中获取Token中的username

非常感谢您的无私开源!

已反复阅读您项目的Readme.md内容,非常感谢。

如果用户携带token放问我,我如何在Controller中获取Token中的username,还是需要用户在提交时额外提交一个参数告诉我他的username呢?

Authorization format

Authorization的格式,对于JWT标准写法应该是Bearer格式吧,而我发现代码中确要求传username。
test
其他问题:
这个jwt的依赖是否能设置token的超时时间?

能否增加注解@hasRole的支持

页面跳转问题

你好,请教下, 按这个结构,页面上超链接跳转,js中location.href跳转时怎么实现权限控制?header中怎么加Authorization字段?

在MyRealm无法使用@Autowired service

通过修改ShiroConfig解决了:

@configuration
public class ShiroConfig {

@Bean
public SecurityManager securityManager() {
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    // 设置realm.
    securityManager.setRealm(myShiroRealm());
    /*
     * 关闭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);
    securityManager.setSubjectDAO(subjectDAO);
    return securityManager;
}

@Bean
public MyRealm myShiroRealm() {
    return new MyRealm();
}

@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
    ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();

    factoryBean.setSecurityManager(securityManager);

    // shiro对于验证不通过的请求,会自动跳转到登录页,这里针对APP,设置为返回一个固定的
    factoryBean.setLoginUrl("/600");

    // 添加自己的过滤器并且取名为token,在下方使用
    Map<String, Filter> filterMap = new HashMap<>();
    filterMap.put("token", new TokenFilter());
    factoryBean.setFilters(filterMap);

    /*
     * 自定义url规则
     * http://shiro.apache.org/web.html#urls-
     */
    Map<String, String> filterRuleMap = new LinkedHashMap<>();
    // 配置不会被拦截的路径 顺序判断
    filterRuleMap.put("/600", "anon");
    filterRuleMap.put("/member/login", "anon");
    // 所有请求通过我们自己的token Filter
    filterRuleMap.put("/**", "token");

    factoryBean.setFilterChainDefinitionMap(filterRuleMap);
    return factoryBean;
}

/**
 * 下面的代码是添加注解支持
 */
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
    return new DefaultAdvisorAutoProxyCreator();
}

@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
    return new LifecycleBeanPostProcessor();
}

@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
    AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
    advisor.setSecurityManager(securityManager);
    return advisor;
}

}

关于Authorization授权的时候出了一个问题。

作者大大你好,我在跑你这个项目的时候。访问/require_auth 这个接口的时候出现了401现象。##### token是拿到了的。。具体的访问内容如下。
  var token;
    $(".bu").click(function() {
      $.post('http://localhost:8080/login', {
        username: 'smith',
        password: 'smith123'

      }, function(data, textStatus, xhr) {
        /*optional stuff to do after success */
        console.log(data);
        token = data.data;
      });
    });

    $(".require_auth").click(function() {
      $.get('http://localhost:8080/require_auth', {
        Authorization: token
      }, function(data, textStatus, xhr) {
        /*optional stuff to do after success */
        console.log(data);
      });
    });

然后在访问这个接口时/require_auth 出现401

  @GetMapping("/require_auth")
    @RequiresAuthentication
    public ResponseBean requireAuth() {

        return new ResponseBean(200, "You are authenticated", null);
    }

反馈

作者大大这是为嘛呀,绝望脸,我那个token那个加错了的意思么

验证失败

使用url拼接登陆,获取token之后,获取不到权限。导致授权失败,无法访问权限页面

整合mybatis事务失效

Application

@SpringBootApplication
@MapperScan(basePackages = {"org.inlighting.dao"})
@EnableTransactionManagement
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application .class);
    }
}

Service

public interface IUserService {
     UserBean getUser(String username) ;
}

ServiceImpl

@Service
public class UserService implements IUserService{
    private final UserMapper mapper;

    @Autowired
    public UserService(UserMapper mapper) {
        this.mapper = mapper;
    }

    @Transactional(rollbackFor = Exception.class)
    public UserBean getUser(String username) {
        // 没有此用户直接返回null
        Map<String, String> detail = mapper.selectUserByName(username);
        if (detail == null )
            return null;

        UserBean user = new UserBean();
        user.setUsername(username);
        user.setPassword(detail.get("USER_PSWD"));
        user.setRole(detail.get("ROLE_NAME"));
        user.setPermission("*");
        return user;
    }
}

log

2018-05-17 10:51:12.517 DEBUG 9948 --- [nio-1024-exec-1] org.mybatis.spring.SqlSessionUtils       : Creating a new SqlSession
2018-05-17 10:51:12.520 DEBUG 9948 --- [nio-1024-exec-1] org.mybatis.spring.SqlSessionUtils       : SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@57f320e4] was not registered for synchronization because synchronization is not active
2018-05-17 10:51:12.856  INFO 9948 --- [nio-1024-exec-1] com.alibaba.druid.pool.DruidDataSource   : {dataSource-1} inited
2018-05-17 10:51:12.860 DEBUG 9948 --- [nio-1024-exec-1] o.i.dao.UserMapper.selectUserByName      : ==>  Preparing: select * from user where userName = ?
2018-05-17 10:51:12.882 DEBUG 9948 --- [nio-1024-exec-1] o.i.dao.UserMapper.selectUserByName      : ==> Parameters: ricky(String)
2018-05-17 10:51:12.905 DEBUG 9948 --- [nio-1024-exec-1] o.i.dao.UserMapper.selectUserByName      : <==      Total: 1
2018-05-17 10:51:12.906 DEBUG 9948 --- [nio-1024-exec-1] org.mybatis.spring.SqlSessionUtils       : Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@57f320e4]

删除 DefaultAdvisorAutoProxyCreator 配置就正常

2018-05-17 11:02:05.302 DEBUG 7380 --- [nio-1024-exec-1] org.mybatis.spring.SqlSessionUtils       : Creating a new SqlSession
2018-05-17 11:02:05.307 DEBUG 7380 --- [nio-1024-exec-1] org.mybatis.spring.SqlSessionUtils       : Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6c7ef00b]
2018-05-17 11:02:05.320 DEBUG 7380 --- [nio-1024-exec-1] o.i.dao.UserMapper.selectUserByName      : ==>  Preparing: SELECT * from user where userName = ?
2018-05-17 11:02:05.342 DEBUG 7380 --- [nio-1024-exec-1] o.i.dao.UserMapper.selectUserByName      : ==> Parameters: ricky(String)
2018-05-17 11:02:05.365 DEBUG 7380 --- [nio-1024-exec-1] o.i.dao.UserMapper.selectUserByName      : <==      Total: 1
2018-05-17 11:02:05.366 DEBUG 7380 --- [nio-1024-exec-1] org.mybatis.spring.SqlSessionUtils       : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6c7ef00b]
2018-05-17 11:02:05.366 DEBUG 7380 --- [nio-1024-exec-1] org.mybatis.spring.SqlSessionUtils       : Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6c7ef00b]
2018-05-17 11:02:05.366 DEBUG 7380 --- [nio-1024-exec-1] org.mybatis.spring.SqlSessionUtils       : Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6c7ef00b]
2018-05-17 11:02:05.366 DEBUG 7380 --- [nio-1024-exec-1] org.mybatis.spring.SqlSessionUtils       : Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6c7ef00b]

有其他的好方法吗?

关于doGetAuthenticationInfo的异常问题

当拿token来请求的时候,存在token过期无效,token本身无效的情况,没想明白为什么要抛出User didn't existed! 和 Username or password error 的异常,两个异常是不是在登陆时抛出更好一些。

关于filter拦截配置问题的请教

您好:
我想问一下,这个项目中没有使用到任何web.xml文件配置filter,也没有使用注入filter用到的FilterRegistrationBean 类,请教为什么可以做到 shiro能拦截到所有请求的,具体实现是在哪里呢,麻烦了~

doGetAuthorizationInfo方法不验证权限

当你发起一个请求之后,进入到自定义的Filter里面判断是否登录,假如你的token是正确的就直接进入该方法了。不会进入到doGetAuthorizationInfo方法的权限验证里面。

难道是我打开的姿势不对?

关于在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所捕获,就导致返回的数据结构不统一了。
请问有解决办法吗?

org.inlighting.controller.WebController#login

这个方法感觉没在shiro的机制中,如果直接访问该方法映射的请求,那么整个系统就相当于进行了两次登录操作(一次是该login本身登录逻辑,一次是shiro拦截器引起的登录逻辑),感觉怪怪的,不知道你的想法是啥

想请教个关于token安全问题

请教大佬一个问题,如果这里的secret换成一个服务器独有的密钥会不会好点?
因为调用者如果知道是这种签名方式,是不是能通过自己的密码来签名,然后通过设置很大的过期时间达到无限请求接口的目的?

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

关于项目本身:

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

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

关于token超时的问题

老铁, 能不能集成jwt的 refreshToken呢 , 现在过期时间是五分钟时间短, 就算延长了1小时的话, 那如果过期了的话, 用户还是要重新登录, 能不能通过不重新登录的方式, 而重新获取一个token呢 ?

token过期

你好,想问下,token过期后会自动登录然后把新的token返回吗?

fix it ?

This subject is anonymous - it does not have any identifying principals and " +
"authorization operations require an identity to check against. A Subject instance will " +
"acquire these identifying principals automatically after a successful login is performed " +
"be executing org.apache.shiro.subject.Subject.login(AuthenticationToken) or when 'Remember Me' " +
"functionality is enabled by the SecurityManager. This exception can also occur when a " +
"previously logged-in Subject has logged out which " +
"makes it anonymous again. Because an identity is currently not known due to any of these " +
"conditions, authorization is denied.

数据库移植出了一点问题

大佬你好,我就是在移植你那个数据,改到MySQL数据库时,出了一个很奇怪的问题

img.这个是地方百度了也是没有找到什么解决办法。

那个Authentication failed for token submission他是出在什么地方啦,大佬有什么解决的办法啊。头大撞墙

权限控制的方法

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

@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"
}

没跑起来

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

关于filter的两个问题请教

我想问下我执行的login方法JWTFilter里面的isLoginAttempt方法返回为啥为false,它的字面意思不是尝试登陆吗,
还有个executeLogin方法一直不会执行。(由于isLoginAttempt是false)我对这不是很理解。大佬指点一下

前后端分离shiro返回用户权限

请问下shiro在认证并授权后返回的SimpleAuthorizationInfo在controller中只能通过SecurityUtils.getSubject().hasRole()去判断认证的用户是否有某个权限而不能拿到这些权限的具体内容对么?查了很多资料,好像都是这样简单地去判断 。
我现在想实现的是返回给前端当前用户的角色以及他能访问的菜单栏的数据(菜单名称,url等),前端vuejs遍历这些返回的数据并渲染出菜单,jwt的部分已经实现了。虽然后台也可以单独地去查数据库然后直接返回,但是这样好像就没用到shiro的realm,请问您知道这个怎么解决么或者这个思路是错了么?

在此工程中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去掉也会正常,但不知道为什么会这样???

登录成功后权限访问不了

git clone 代码,启动运行成功

  1. 先访问login进行登录操作
  2. 登录成功后,访问/article还是返回You are guest.
  3. 登录成功后,访问/require_auth返回401.
    请问下是不是还有什么需要配置的

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");

但我不太清楚为什么

anon好像不起作用 ?

Map<String, String> filterRuleMap = new HashMap<>();
// 访问401和404页面不通过我们的Filter
filterRuleMap.put("/401", "anon");
filterRuleMap.put("/login", "anon");
// 所有请求通过我们自己的JWT Filter
filterRuleMap.put("/**", "jwt");

    factoryBean.setFilterChainDefinitionMap(filterRuleMap);
    return factoryBean;

只要请求头里有Authorization就会去执行认证。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.