SpringBoot+Shiro (一)

从网上搜索SpringBoot+Shiro相关文章,大部分都需要DB和Ecache的支持。这里提供一个最简单的Spring+Shiro的配置。

前言:

1. 由于SpringBoot官方已经不再建议使用jsp,并且前后端分离及服务化的大趋势也越来越强烈,所以本文旨在搭建一个Restfull的web服务。

2. Rest接口的授权基于Shiro注解的方式实现,这样更灵活更容易掌控。

pom依赖:

这是最小化的依赖项,缺一不可。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</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-aop</artifactId>
</dependency>

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

创建一个Realm

这里面用户认证和授权没有通过连接DB的方式实现,用户信息(用户名、密码)以及用户权限直接硬编码到了代码里。从代码可看出这个例子只支持两个用户:admin/admin、guest/guest,且admin用户拥有角色admin和权限permission1、permission2,guest用户拥有角色guest和权限permission3、permission4.

public class PropertiesRealm extends AuthorizingRealm {
    
    // 用户认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        if (authenticationToken instanceof UsernamePasswordToken) {
            UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
            String username = token.getUsername();
            String password = new String(token.getPassword());
            if ((username.equals("admin") && password.equals("admin")) ||
                    (username.equals("guest") && password.equals("guest"))) {
                return new SimpleAuthenticationInfo(username, password, getName());
            } else {
                throw new AuthenticationException("用户名或密码错误");
            }
        } else if (authenticationToken instanceof RememberMeAuthenticationToken) {
            RememberMeAuthenticationToken token = (RememberMeAuthenticationToken) authenticationToken;
            // TODO: 2018/10/24
            return null;
        } else if (authenticationToken instanceof HostAuthenticationToken) {
            HostAuthenticationToken token = (HostAuthenticationToken) authenticationToken;
            // TODO: 2018/10/24
            return null;
        } else {
            throw new AuthenticationException("未知的AuthenticationToken类型");
        }
    }

    // 用户授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        String username = (String) principalCollection.getPrimaryPrincipal();
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        if (username.equals("admin")) {
            simpleAuthorizationInfo.addRole("admin");
            simpleAuthorizationInfo.addStringPermission("permission1");
            simpleAuthorizationInfo.addStringPermission("permission2");
        } else if (username.equals("guest")) {
            simpleAuthorizationInfo.addRole("guest");
            simpleAuthorizationInfo.addStringPermission("permission3");
            simpleAuthorizationInfo.addStringPermission("permission4");
        }
        return simpleAuthorizationInfo;
    }
}

创建SpringBootApplication

下面这段代码中,需要注意的就是ShiroFilterFactoryBean和SimpleMappingExceptionResolver这两个bean:

1. SimpleMappingExceptionResolver:Shiro通过注解的方式校验用户认证和授权时,如果用户未认证或权限不足,Shiro不会进行页面跳转,而是直接抛出异常。所以我们需要定义SimpleMappingExceptionResolver来处理这两个异常,以保证我们的rest接口在用户未认证或权限不足的时候返回正确的json数据。

2. ShiroFilterFactoryBean:这个bean不需要setSuccessUrl()、setLoginUrl()和setUnauthorizedUrl()。因为Shiro通过注解的方式校验用户认证和授权时,如果用户未认证或权限不足,Shiro不会进行页面跳转,而是直接抛出异常,所以setLoginUrl()和setUnauthorizedUrl()就不需要了。由于我们是做Rest接口服务,那么用户认证过程中也是调用Rest API校验用户身份,校验通过后由前端页面路由至登录成功页面,所以setSuccessUrl()也不需要了。

@SpringBootApplication
public class ShiroApplication {

    public static void main(String[] args) {
        SpringApplication.run(ShiroApplication.class);
    }

    @Bean
    public PropertiesRealm propertiesRealm() {
        return new PropertiesRealm();
    }

    @Bean
    public SecurityManager securityManager(PropertiesRealm propertiesRealm) {
        return new DefaultWebSecurityManager(propertiesRealm);
    }

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        return shiroFilterFactoryBean;
    }

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

    @Bean
    public SimpleMappingExceptionResolver simpleMappingExceptionResolver() {
        Properties properties = new Properties();
        properties.put(UnauthenticatedException.class.getName(), "/unauthenticated");
        properties.put(UnauthorizedException.class.getName(), "/unauthorized");

        SimpleMappingExceptionResolver simpleMappingExceptionResolver = new SimpleMappingExceptionResolver();
        simpleMappingExceptionResolver.setExceptionMappings(properties);
        return simpleMappingExceptionResolver;
    }

}

创建统一的Restfull返回结果包装:

public class Result {

    private int code = 200;
    private String message = "success";
    private Object data;

    public Result() {
    }

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

    public Result(Object data) {
        this.data = data;
    }

    public int getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }

    public Object getData() {
        return data;
    }
}

创建Controler

方法说明:

  • unauthenticated:根据SimpleMappingExceptionResolver的配置,如果Shiro检测方法请求需要用户登录,则会重定向到/unauthenticated,返回401“需要登录”,以便前端根据状态码做相应的路由跳转。
  • unauthorized:根据SimpleMappingExceptionResolver的配置,如果Shiro检测到用户权限不足,则会重定向到/unauthorized,返回401“未授权”,以便前端根据状态码做相应的路由跳转。
  • login:用户登录
  • logout:用户登出
  • getData1、getData2:用来做测试用。getData1需要用户认证,用户完成认证后即可访问;getData2需要用户拥有“admin”角色才能访问。
@RestController
public class LoginController {


    @GetMapping("unauthenticated")
    public Result unauthenticated() {
        return new Result(401, "需要登录");
    }

    @GetMapping("unauthorized")
    public Result unauthorized() {
        return new Result(401, "未授权");
    }

    @PostMapping("login")
    public Result login(String username, String password) {
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        subject.login(token);
        return new Result();
    }

    @PostMapping("logout")
    public Result logout() {
        Subject subject = SecurityUtils.getSubject();
        subject.logout();
        return new Result();
    }


    @GetMapping("getData1")
    @RequiresUser
    public Result getData1() {
        return new Result("this is data1");
    }

    @GetMapping("getData2")
    @RequiresRoles("admin")
    public Result getData2() {
        return new Result("this is data2");
    }
}