从网上搜索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"); } }