基于SpringBoot 开发Restful风格的API

代码以上传码云 https://gitee.com/HuiSeChengXuYuan/shiro-demo

1.基于Spring boot 2.1.3 开发
2.接口文档使用Swagger
3.权限控制框架使用Shiro

Maven 依赖:

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.3.RELEASE</version>
        <relativePath/> 
</parent>
<dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.2</version>
        </dependency>
        <!--swagger-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.4.0</version>
        </dependency>

        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.4.0</version>
        </dependency>

        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
            <version>1.2.4</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.0</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

Shiro介绍

Shiro是一个强大易用的Java安全框架,提供了认证、授权、加密和会话管理等功能。


Authentication: 身份认证/登录,验证用户是不是拥有相应的身份;

Authorization: 授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;

Session Manager: 会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境的,也可以是如Web环境的;

Cryptography: 加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;

Web Support: Web支持,可以非常容易的集成到Web环境;

**Caching:**缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;

Concurrency: shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;

Testing: 提供测试支持;

Run As: 允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;

Remember Me: 记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。

记住一点,Shiro不会去维护用户、维护权限;这些需要我们自己去设计/提供;然后通过相应的接口注入给Shiro即可。

shiro鉴权过程

首先,我们从外部来看Shiro吧,即从应用程序角度的来观察如何使用Shiro完成工作。如下图:

可以看到:应用代码直接交互的对象是Subject,也就是说Shiro的对外API核心就是Subject;其每个API的含义:

Subject:主体,代表了当前“用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等;即一个抽象概念;所有Subject都绑定到SecurityManager,与Subject的所有交互都会委托给SecurityManager;可以把Subject认为是一个门面;SecurityManager才是实际的执行者;

SecurityManager:安全管理器;即所有与安全有关的操作都会与SecurityManager交互;且它管理着所有Subject;可以看出它是Shiro的核心,它负责与后边介绍的其他组件进行交互,如果学习过SpringMVC,你可以把它看成DispatcherServlet前端控制器;

Realm:域,Shiro从从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源。

shiro架构

Subject:主体,可以看到主体可以是任何可以与应用交互的“用户”;

SecurityManager:相当于SpringMVC中的DispatcherServlet或者Struts2中的FilterDispatcher;是Shiro的心脏;所有具体的交互都通过SecurityManager进行控制;它管理着所有Subject、且负责进行认证和授权、及会话、缓存的管理。

Authenticator:认证器,负责主体认证的,这是一个扩展点,如果用户觉得Shiro默认的不好,可以自定义实现;其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了;

Authrizer:授权器,或者访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能;

Realm:可以有1个或多个Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是JDBC实现,也可以是LDAP实现,或者内存实现等等;由用户提供;注意:Shiro不知道你的用户/权限存储在哪及以何种格式存储;所以我们一般在应用中都需要实现自己的Realm;

SessionManager:如果写过Servlet就应该知道Session的概念,Session呢需要有人去管理它的生命周期,这个组件就是SessionManager;而Shiro并不仅仅可以用在Web环境,也可以用在如普通的JavaSE环境、EJB等环境;所有呢,Shiro就抽象了一个自己的Session来管理主体与应用之间交互的数据;这样的话,比如我们在Web环境用,刚开始是一台Web服务器;接着又上了台EJB服务器;这时想把两台服务器的会话数据放到一个地方,这个时候就可以实现自己的分布式会话(如把数据放到Memcached服务器);

SessionDAO:DAO大家都用过,数据访问对象,用于会话的CRUD,比如我们想把Session保存到数据库,那么可以实现自己的SessionDAO,通过如JDBC写到数据库;比如想把Session放到Memcached中,可以实现自己的Memcached SessionDAO;另外SessionDAO中可以使用Cache进行缓存,以提高性能;

CacheManager:缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能

Cryptography:密码模块,Shiro提高了一些常见的加密组件用于如密码加密/解密的。

废话不多说直接上代码

项目结构:

rest_framework 权限 restful api 权限控制_缓存

Swagger 配置类

@Configuration
@EnableSwagger2
public class SwaggerConfig {

    @Value("${swagger.show}")
    private Boolean enable;

    @Bean
    public Docket swaggerSpringMvcPlugin() {
        //在请求头中添加一个为 Authorization 的属性 shiro拦截器 认证的时候会用到
        Parameter parameter = new ParameterBuilder()
                .name("token")
                .description("token")
                .modelRef(new ModelRef("string"))
                .parameterType("header")
                .required(false)
                .build();

        return new Docket(DocumentationType.SWAGGER_2)
                .enable(enable)
                .groupName("demo")
                .apiInfo(apiInfo())
                .globalOperationParameters(Collections.singletonList(parameter))
                .select()
                .apis(withClassAnnotation(Api.class))
                .paths(this.paths())
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("Shiro集成")
                .description("接口")
                .version("1.0")
                .build();
    }


    public static Predicate<String> paths() {
        return Predicates.alwaysTrue();
    }
}

Spring MVC 全局异常处理类

@RestControllerAdvice
public class GlobalControllerExceptionHandler {

    @ResponseStatus(HttpStatus.UNAUTHORIZED)//response状态码
    @ExceptionHandler(AuthorizationException.class)//捕捉的Shiro异常
    public Result AuthorizationExceptionHandler(AuthorizationException e) {
        Result result = new Result(String.valueOf(HttpStatus.UNAUTHORIZED.value()), "权限不足");
        return result;
    }

}

Result类

@Data
public class Result {

    public Result(String code, String message) {
        this.code = code;
        this.message = message;
    }

    //错误编码
    private String code;
    //错误信息
    private String message;

}

ShiroConfig类配置

@Configuration
public class ShiroConfig {

    @Bean("shiroFilter")
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager);//设置 securityManager
        Map<String, String> filterChainDefinitionMapping = shiroFilter.getFilterChainDefinitionMap();
        setUrl(filterChainDefinitionMapping, "anon", AnonUrl());//不需要拦截的路径
        setUrl(filterChainDefinitionMapping, "method,auth", AuthUrl());//需要被拦截的路径 这里的 method、auth 和下面的拦截器对应

        Map<String, Filter> filterMap = new HashMap<>();
        //MethodFilter TokenFilter 自定义拦截器
        filterMap.put("method", new MethodFilter());// cors自己随便定义,写abc也行 个人喜欢
        filterMap.put("auth", new TokenFilter());// auth自己随便定义,写abc也行 个人喜欢
        shiroFilter.setFilters(filterMap);
        return shiroFilter;
    }

    private void setUrl(Map<String, String> filterChainDefinitionMapping, String filterName, List<String> urlList) {
        if (!urlList.isEmpty()) {
            Iterator<String> iterator = urlList.iterator();
            while (iterator.hasNext()) {
                String url = iterator.next();
                filterChainDefinitionMapping.put(url, filterName);//
            }
        }
    }

    //不对swagger访问路径拦截
    private List<String> AnonUrl() {
        List<String> list = new ArrayList<>();
        list.add("/swagger*");
        list.add("/api/test/login");
        return list;
    }

    //对拦截 /api/** 的API进行拦截
    private List<String> AuthUrl() {
        List<String> list = new ArrayList<>();
        list.add("/api/**");
        return list;
    }


    @Bean("securityManager")
    public SecurityManager securityManager(TokenRealm tokenRealm) {
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        manager.setRealm(tokenRealm);//设置域对象
        DefaultSubjectDAO de = (DefaultSubjectDAO) manager.getSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = (DefaultSessionStorageEvaluator) de.getSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);//禁用Session存储
        StatelessDefaultSubjectFactory statelessDefaultSubjectFactory = new StatelessDefaultSubjectFactory();
        manager.setSubjectFactory(statelessDefaultSubjectFactory);
        manager.setSessionManager(this.defaultSessionManager());
        SecurityUtils.setSecurityManager(manager);
        return manager;
    }

    @Bean("tokenRealm")
    public TokenRealm tokenRealm() {
        return new TokenRealm();
    }

    @Bean
    public DefaultSessionManager defaultSessionManager() {
        DefaultSessionManager manager = new DefaultSessionManager();
        manager.setSessionValidationSchedulerEnabled(false);//禁用Session
        return manager;
    }

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

        return authorizationAttributeSourceAdvisor;
    }

    //启用Shiro注解
    @Bean
    @DependsOn({"lifecycleBeanPostProcessor"})
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }

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

MethodFilter

public class MethodFilter extends AbstractFilter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        System.out.println("请求方法 Method : " + request.getMethod());
        filterChain.doFilter(servletRequest,servletResponse);
    }
}

TokenFilter

//需要认证的API被调用前执行的拦截器
public class TokenFilter extends AuthenticationFilter {

    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        String token = getToken(servletRequest);
        if (StringUtils.isEmpty(token)) {
            return false;
        } else {
            boolean isSuccess = this.login(token);
            if (!isSuccess) {
                this.printUnauthorized("401", (HttpServletResponse) servletResponse);
            }
            return isSuccess;
        }
    }

    private boolean login(String token) {
        try {
            Subject subject = SecurityUtils.getSubject();
            subject.login(new Token(token));//subject.login() 调用 自定义的 TokenRealm 对象,进行认证和授权。
            return true;
        } catch (AuthenticationException e) {
//            e.printStackTrace();
            return false;
        }

    }

    private String getToken(ServletRequest servletRequest) {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String authorizationHeader = request.getHeader("token");//获取请求头中的Authorization属性
        if (!StringUtils.isEmpty(authorizationHeader)) {
            return authorizationHeader.replace(" ", "");
        }
        return null;
    }

    private void printUnauthorized(String messageCode, HttpServletResponse response) {
        String content = String.format("{\"code\":\"%s\",\"msg\":\"%s\"}", messageCode, HttpStatus.UNAUTHORIZED.getReasonPhrase());
        response.setContentType("application/json;charset=UTF-8");
        response.setContentLength(content.length());
        response.setStatus(HttpStatus.UNAUTHORIZED.value());

        try {
            PrintWriter writer = response.getWriter();
            writer.write(content);
        } catch (IOException var5) {
            var5.printStackTrace();
        }

    }
}

Token

//自定义Token 实现AuthenticationToken 认证是需要用到
public class Token implements AuthenticationToken {
    private String token;

    Token(String token) {
        this.token = token;
    }

    String getToken() {
        return this.token;
    }

    @Override
    public Object getPrincipal() {
        return this.getToken();
    }

    @Override
    public Object getCredentials() {
        return this.getToken();
    }
}

StatelessDefaultSubjectFactory

//subject工厂
public class StatelessDefaultSubjectFactory extends DefaultWebSubjectFactory {

    public Subject createSubject(SubjectContext context) {
        context.setSessionCreationEnabled(false);//禁用Session
        return super.createSubject(context);
    }
}

TokenRealm

//自定义域对象,实现认证和授权的方法
public class TokenRealm extends AuthorizingRealm {

    @Resource(name = "stringRedisTemplate")
    private ValueOperations<String, String> redis;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    public String getName() {
        return "Realm";
    }

    @Override
    public boolean supports(AuthenticationToken token) {
        return token != null && Token.class.isAssignableFrom(token.getClass());
    }

    //认证 Authentication
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        System.out.println("认证 Authentication");
        Token token = (Token) authenticationToken;
        String role = redis.get("random.token." + token.getToken());//获取对应token的角色
        if (!StringUtils.isEmpty(role)) {
            return new SimpleAuthenticationInfo(role, token.getToken(), this.getName());
        }
        return null;
    }

    //授权 Authorization
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
        System.out.println("授权 Authorization");
        String role = (String) principal.getPrimaryPrincipal();
        if (!StringUtils.isEmpty(role)) {
            SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
            info.addRole(role);
            return info;
        }
        return null;
    }
}

TestController

@RestController
@RequestMapping("/api/test")
@Api(tags = "测试模块")
public class TestController {

    @Resource(name = "stringRedisTemplate")
    private ValueOperations<String, String> redis;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    private String[] tokens = {"token1", "token2", "token3"};

    private String[] roles = {"admin", "user"};

    @ApiOperation("测试")
    @GetMapping("/getAdmin")
    @RequiresRoles(logical = Logical.OR, value = {"admin"})//shiro注解 admin角色
    public String getAdmin() {
        return "Admin测试成功";
    }

    @ApiOperation("测试")
    @GetMapping("/getUser")
    @RequiresRoles(logical = Logical.OR, value = {"user"})//shiro注解 user角色
    public String getUser() {
        return "User测试成功";
    }

    @ApiOperation("登录")
    @PostMapping("/login")
    public Map<String, String> login(@RequestParam @ApiParam String name, @RequestParam @ApiParam String password) {
        String token = "";
        String role = "";
        if (name.equals("user") && password.equals("123456")) {
            Random random = new Random();
            token = tokens[random.nextInt(tokens.length)];//随机token ,开发项目可用随机字符替代
            role = roles[random.nextInt(roles.length)];//随机角色
            redis.set("random.token." + token, role); //将token和token对应的role存入 Redis中
            Map<String, String> map = new HashMap<>();
            map.put("token", token);
            map.put("role", role);
            return map;
        }
        return null;
    }
}

application.yml

spring:
  application:
    name: demo
  redis:
    host: 127.0.0.1
    port: 6379
    password: 123456
    database: 1
    timeout: 5000ms
    jedis:
      pool:
        min-idle: 1
        max-idle: 1
        max-active: 2
        max-wait: -1ms
server:
  port: 80
swagger:
  show: true

测试效果

rest_framework 权限 restful api 权限控制_Web_02


rest_framework 权限 restful api 权限控制_spring_03


rest_framework 权限 restful api 权限控制_spring_04


rest_framework 权限 restful api 权限控制_rest_framework 权限_05

参考文章

  • 作者:clj198606061111