在Web应用中安全问题同样不可忽视,所以Spring Framework体系中也为此提供了解决方案——Spring Security。但是由于其较为复杂,新人上手较为困难。所以这里我们来介绍另外一个简单易用的开源安全框架——Apache Shiro,并说明如何在SpringBoot中实现身份认证、权限授权
配置
Maven依赖
在Pom.xml添加Shiro Framework依赖
1 2 3 4 5 6 7 8 9 10 11 12 13
| <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring-boot-starter</artifactId> <version>1.4.0</version> </dependency>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> <version>2.1.4.RELEASE</version> </dependency>
|
自定义令牌
相比较每次HTTP请求通过用户名、密码来进行身份认证,使用后端生成的Token则会更加安全、方便,为此我们需要先自定义一个基于Token的令牌以用于身份认证,如下所示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
|
public class CustomToken implements AuthenticationToken {
public static String TOKENHEARDNAME = "token";
private String token;
public CustomToken(String token) { this.token = token; }
@Override public Object getPrincipal() { return token; }
@Override public Object getCredentials() { return token; } }
|
自定义拦截器 Filter
为保证Web应用安全,不被非法访问攻击。我们还需要自定义一个拦截器Filter,其作用就是使得HTTP请求在到达指定的Contorller之前被拦截以便进行认证、授权等任务。这里我们通过继承AuthenticatingFilter定义了一个我们自己的拦截器CustomFilter。在拦截器中,如果我们发现请求头中包含token字段(携带令牌),则予以放行说明该请求可以进行认证、授权任务;反之则在拦截器中直接过滤掉该请求
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
|
public class CustomFilter extends AuthenticatingFilter {
@Override protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception { String token = getTokenFromHeard(servletRequest); if( token == null ) { return null; } return new CustomToken(token); }
@Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { return false; }
@Override protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception { String token = getTokenFromHeard(servletRequest); if( token==null ) { HttpServletResponse httpServletResponse = (HttpServletResponse)servletResponse; httpServletResponse.setStatus(401); httpServletResponse.getWriter().write("Error: No Token Message"); return false; } return executeLogin(servletRequest, servletResponse); }
@Override protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) { HttpServletResponse httpServletResponse = (HttpServletResponse)response; httpServletResponse.setStatus(401); try { httpServletResponse.getWriter().write( e.getMessage() ); } catch (IOException e1) { } return false; }
private String getTokenFromHeard(ServletRequest request) { return ((HttpServletRequest)request).getHeader(CustomToken.TOKENHEARDNAME); } }
|
自定义Realm
在Shiro中有一个Realm的概念,通过它来可以完成认证、授权等任务的具体逻辑。一般地,我们需要根据自己的实际业务需求来实现它,换句话说,Realm就相当于是一个安全相关的Dao。这里我们通过继承AuthorizingRealm类实现了一个自定义的Realm类,我们需要重写父类的doGetAuthenticationInfo、doGetAuthorizationInfo方法来定义我们的身份认证、权限授权的具体逻辑。在前文中,我们定义了一个自定义令牌——CustomToken,所以这里通过重写supports方法来只使用我们自定义的令牌
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
|
public class CustomRealm extends AuthorizingRealm {
@Autowired private UserTokenService userTokenService;
@Autowired private UserService userService; @Autowired private RolePermService rolePermService;
@Override public boolean supports(AuthenticationToken token) { return token instanceof CustomToken; }
@Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { String token = (String)authenticationToken.getPrincipal();
Map param = new HashMap(); param.clear(); param.put("token", token); List<UserToken> userTokenList = userTokenService.findList(param); if( userTokenList==null || userTokenList.size() != 1 ) { throw new AuthenticationException("Error: Invalid Token"); } Integer userId = userTokenList.get(0).getUserId(); if( userId==null || userId <=0) { throw new AuthenticationException("Error: Invalid Token"); } User user = userService.findById(userId); if( user==null ) { throw new AuthenticationException("Error: Invalid Token"); } AuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user, token, getName()); return authenticationInfo; }
@Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { User user = (User) principalCollection.getPrimaryPrincipal(); RolePerm rolePerm = rolePermService.findById( user.getRoleId() );
Set<String> roleNameSet = new HashSet<>(); roleNameSet.add( rolePerm.getName() );
Set<String> permNameSet = new HashSet<>(); permNameSet.add( rolePerm.getPerm() );
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); info.setRoles( roleNameSet ); info.setStringPermissions( permNameSet ); return info; } }
|
配置Shiro
至此,我们已经把Shiro的相关组件——Token、Filter、Realm,均已经配置完成,现在只需将它们装配在一起即可。示例如下所示,除login登录接口(该接口接收用户名、密码信息来返回token)外,其他接口均需要先进行身份认证
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
|
@Configuration public class ShiroConfig {
@Bean(name = "customRealm") public CustomRealm getCustomRealm() { return new CustomRealm(); }
@Bean(name = "securityManager") public DefaultWebSecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm( getCustomRealm() ); return securityManager; }
@Bean(name = "shiroFilter") public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager);
Map<String, Filter> filters = new HashMap(2); filters.put("customFilter", new CustomFilter() ); shiroFilterFactoryBean.setFilters(filters);
Map<String, String> urlFilterMap = new LinkedHashMap<>(); urlFilterMap.put("/login", "anon"); urlFilterMap.put("/**", "customFilter"); shiroFilterFactoryBean.setFilterChainDefinitionMap(urlFilterMap);
return shiroFilterFactoryBean; }
@Bean public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); }
@Bean @DependsOn("lifecycleBeanPostProcessor") public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator(){ DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator(); defaultAdvisorAutoProxyCreator.setProxyTargetClass(true); return defaultAdvisorAutoProxyCreator; }
@Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } }
|
身份认证
所谓认证就如我们在CustomRealm的doGetAuthenticationInfo方法中的逻辑一样,即判断该请求携带的Token数据是否能查询到相关的用户。如果能则说明身份认证成功;反之则不能。这里问为认证测试提供了一个Controller
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| @RestController @ResponseBody public class ShiroAuthenTestController {
@Autowired private UserService userService;
@Autowired private UserTokenService userTokenService;
@GetMapping("/login") public String login(String username, String password) { String msg = "登录失败"; if( StrUtil.isBlank(username) || StrUtil.isBlank(password) ) { return msg; } User user = userService.findByLoginMsg(username, password); if( user==null || user.getId()<=0 ) { return msg; } Map param = new HashMap<>(); param.put("userId", user.getId()); List<UserToken> userTokens = userTokenService.findList(param); if( userTokens==null || userTokens.size()!=1 ) { return msg; } msg = "Token: " + userTokens.get(0).getToken(); return msg; }
@GetMapping("/testAuthen") public String testAuthen() {
Subject subject = SecurityUtils.getSubject(); User user = (User) subject.getPrincipal(); System.out.println("Login Msg: " + user);
String msg = "test Shiro success"; return msg; } }
|
现在我们通过PostMan来进行测试
1. 请求login接口
通过登录接口分别获取到用户Aaron、Bob的token信息为abcdefg、kfc
2. 请求testAuthen接口
当我们不携带token、携带错误的token发起请求时,可以看到会导致认证失败,接口没有响应
当我们携带正确的token发起请求时,则可以看到认证成功,接口正常响应
权限控制
这里为了便于演示,我们建立了一个简单的用户-权限表,如下图所示
从中我们可以得知:
- 用户Aaron,其Token为abcdefg,角色为user用户,权限为accessPerm1
- 用户Bob,其Token为kfc,角色为root用户,权限为accessPerm2
基于角色的授权
正如我们在CustomRealm的doGetAuthorizationInfo方法中的所做的那样,我们在认证通过之后会为其添加角色、权限相关的信息。在Shiro中可以通过@RequiresRoles注解来方便地表示访问Controller需要的角色条件,这里我们提供了testAuthor1、testAuthor2两个请求接口,前者只能是user用户可以访问,而后者只能是root用户才可以访问
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| @RestController @ResponseBody public class ShiroAuthorTestController {
@GetMapping("/testAuthor1") @RequiresRoles("user") public String testAuthor1() { return "Msg: success access by User"; }
@GetMapping("/testAuthor2") @RequiresRoles("root") public String testAuthor2() { return "Msg: success access by User"; } }
|
现在我们通过PostMan来进行测试testAuthor1接口:
- 当我们不携带token、携带错误的token发起请求时,可以看到会同样导致认证失败,接口没有响应
- 当Aaron访问时,由于其角色为user用户,故接口被正确响应
- 当Bob访问时,虽然认证通过了,但是由于其角色为root用户,没有权限访问该接口,故该接口没有响应
基于权限的授权
在Shiro中也可以通过@RequiresPermissions注解来方便地表示访问Controller需要的权限条件,这里我们提供了testAuthor3、testAuthor4两个请求接口,前者只能是拥有accessPerm1权限的用户可以访问,而后者只能是拥有accessPerm2权限的用户可以访问
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| @RestController @ResponseBody public class ShiroAuthorTestController {
@GetMapping("/testAuthor3") @RequiresPermissions("accessPerm1") public String testAuthor3() { return "Msg: success access by Perm [accessPerm1] "; }
@GetMapping("/testAuthor4") @RequiresPermissions("accessPerm2") public String testAuthor4() { return "Msg: success access by Perm [accessPerm2] "; } }
|
现在我们通过PostMan来进行测试testAuthor3接口:
- 当Aaron访问时,由于其权限条件为accessPerm1,故接口被正确响应
- 当Bob访问时,虽然认证通过了,但是由于其只有accessPerm2的权限条件,故该接口没有响应