1.简介
Apache Shiro是Java的一个安全框架,可用于用认证,授权,加密,会话管理等多个方面,其基本功能点如下图所示:
模块 | 用途 |
---|---|
Authenication | 身份认证/登录,验证用户是否拥有相应身份 |
Authorization | 授权,权限验证,验证某个用户是否拥有某个权限 |
Session Manager | 会话管理 |
Cryptography | 加密 |
Web Support | web支持 |
Caching | 缓存 |
Concurrency | 支持多线程应用并发验证 |
Shiro工作流程如下图所示:
应用程序交互主体为Subject,其中Subject可以看做门面,接收请求后实际交给SecurityManager进行处理,SecurityManager为具体验证逻辑,而Realm为数据源来源,可以看做db
2.SpringBoot整合Shiro
-
数据库准备
-- 用户token表 create table shiro.user_token ( user_id bigint not null primary key, token varchar(100) not null comment 'token', expire_time datetime null comment '过期时间', update_time datetime null comment '更新时间', constraint token unique (token) ) -- 用户表 create table shiro.user ( user_id bigint auto_increment primary key, user_name varchar(32) not null comment '用户名', password varchar(64) not null comment '用户密码', mobile varchar(32) null comment '手机号', salt varchar(64) null comment '盐', locked smallint(6) null comment '是否被锁定(0:锁定,1:正常)', create_user_id bigint null comment '创建人', create_time datetime default CURRENT_TIMESTAMP null comment '创建时间', email varchar(32) null, constraint user_user_name_uindex unique (user_name) ) comment '用户表'; -- 角色表 create table shiro.role ( role_id bigint auto_increment comment '权限id' primary key, role_name varchar(32) not null, remark varchar(64) null comment '权限说明', create_user_id bigint not null comment '创建者id', create_time datetime default CURRENT_TIMESTAMP null comment '创建时间 ' ) comment '权限表'; -- 用户角色表 create table shiro.user_role ( id bigint auto_increment primary key, user_id bigint not null, role_id bigint not null ) comment '用户权限中间表'; -- 菜单表 create table shiro.menu ( menu_id bigint auto_increment primary key, name varchar(32) not null comment '菜单名', parent_id bigint not null comment '父菜单id,顶级菜单为0', perms varchar(32) null comment '权限集合,多个以","分割', parent_name varchar(32) null ) comment '菜单表'; -- 用户菜单表 create table shiro.role_menu ( id bigint auto_increment primary key, menu_id bigint not null, role_id bigint not null )@Configuration public class FilterConfig { @Bean public FilterRegistrationBean shiroFilterRegistration(){ FilterRegistrationBean registration = new FilterRegistrationBean(); registration.setFilter(new DelegatingFilterProxy("shiroFilter")); //该值缺省为false,表示生命周期由SpringApplicationContext管理,设置为true则表示由ServletContainer管理 registration.addInitParameter("targetFilterLifecycle", "true"); registration.setEnabled(true); registration.setOrder(Integer.MAX_VALUE - 1); registration.addUrlPatterns("/*"); return registration; } } comment '权限菜单表';
-
数据准备
-- 创建初始用户 INSERT INTO shiro.user (user_id, user_name, password, mobile, salt, locked, create_user_id, create_time, email) VALUES (1, 'admin', 'da73d08539c91cdf2f11da4f6dcc5dbc2dd9dc8e1647cf1520e3540e77dc2c3c', '15123625205', '932c2d40-ea59-11e9-a2ad-0235d2b38928', 1, 1, '2019-10-09 05:59:51', null); -- 创建菜单权限 INSERT INTO shiro.menu (menu_id, name, parent_id, perms, parent_name) VALUES (1, '新增', 2, 'sys:user:save,sys:role:select', null);
-
springboot环境搭建
springboot基本搭建很简单,在此就不在搭建,只列出所需的相关依赖:
spring-boot-starter-aop
,lombok
,druid-spring-boot-starter
,mysql-connector-java
,mybatis-plus-boot-starter
,joda-time
,cos_api
-
引入
shiro
依赖<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.4.0</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.4.0</version> </dependency>
-
shiro相关配置
(1)
shiroConfig
:主要用于设置securityManager
及请求拦拦截配置过滤器@Configuration public class ShiroConfig { @Bean("securityManager") public SecurityManager securityManager(ShiroRealm shiroRealm) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(shiroRealm); securityManager.setRememberMeManager(null); return securityManager; } @Bean("shiroFilter") public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean(); shiroFilter.setSecurityManager(securityManager); //shiro过滤 Map<String, Filter> filters = new HashMap<>(); filters.put("shiro", new ShiroFilter()); shiroFilter.setFilters(filters); Map<String, String> filterMap = new LinkedHashMap<>(); filterMap.put("/webjars/**", "anon"); filterMap.put("/druid/**", "anon"); filterMap.put("/app/**", "anon"); filterMap.put("/sys/login", "anon"); filterMap.put("/swagger/**", "anon"); filterMap.put("/v2/api-docs", "anon"); filterMap.put("/swagger-ui.html", "anon"); filterMap.put("/swagger-resources/**", "anon"); filterMap.put("/captcha.jpg", "anon"); filterMap.put("/aaa.txt", "anon"); filterMap.put("/**", "shiro"); shiroFilter.setFilterChainDefinitionMap(filterMap); return shiroFilter; } @Bean("lifecycleBeanPostProcessor") public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor( SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor(); advisor.setSecurityManager(securityManager); return advisor; } }
(2)
shiroFilter
:自定义过滤器该过滤器主要定义了请求成功,或登录失败后的处理措施
public class ShiroFilter extends AuthenticatingFilter { @Override protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception { String token = getRequestToken((HttpServletRequest) request); if (StringUtils.isEmpty(token)) { return null; } return new ShiroToken(token); } private String getRequestToken(HttpServletRequest request) { //从header中获取token String token = request.getHeader("token"); //如果header中不存在token,则从参数中获取token if (StringUtils.isEmpty(token)) { token = request.getParameter("token"); } return token; } @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { if (((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name())) { return true; } return false; } @Override protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception { String token = getRequestToken((HttpServletRequest) servletRequest); if (StringUtils.isEmpty(token)) { HttpServletResponse httpResponse = (HttpServletResponse) servletResponse; httpResponse.setHeader("Access-Control-Allow-Credentials", "true"); httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin()); String json = new Gson().toJson(ExtResult.error(HttpStatus.SC_UNAUTHORIZED, "invalid token")); httpResponse.getWriter().print(json); return false; } return executeLogin(servletRequest, servletResponse); } @Override protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) { HttpServletResponse httpResponse = (HttpServletResponse) response; httpResponse.setContentType("application/json;charset=utf-8"); httpResponse.setHeader("Access-Control-Allow-Credentials", "true"); httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin()); try { //处理登录失败的异常 Throwable throwable = e.getCause() == null ? e : e.getCause(); ExtResult r = ExtResult.error(HttpStatus.SC_UNAUTHORIZED, throwable.getMessage()); String json = new Gson().toJson(r); httpResponse.getWriter().print(json); } catch (IOException e1) { } return false; } }
(3)
shiorToken
:用于自定义token,后续所有请求均依赖于token校验public class ShiroToken implements AuthenticationToken { private String token; public ShiroToken(String token) { this.token = token; } @Override public Object getPrincipal() { return token; } @Override public Object getCredentials() { return token; } }
(4) token生成器,用于生成token
public class TokenGenerator { public static String generateValue() { return generateValue(UUID.randomUUID().toString()); } public static String generateValue(String param) { try { //指定算法,初始化对象 Provider[] providers = Security.getProviders(); System.out.println(providers); MessageDigest md5 = MessageDigest.getInstance("MD5"); //重置摘要 md5.reset(); //处理数据 md5.update(param.getBytes()); //调用digest将MessageDigest设置成初始化状态,digest()只能调用一次,因此要初始化 byte[] digest = md5.digest(); return toHexString(digest); } catch (Exception e) { throw new ExtException("生成token失败", e); } } private static final char[] hexCode = "0123456789abcdef".toCharArray(); private static String toHexString(byte[] data) { if(Objects.isNull(data)){ return null; } StringBuilder sb = new StringBuilder(data.length * 2); for (byte datum : data) { //对每个数字做位运算,只有都为1时结果才为1,否则为0,然后取出数组中对应下标的数 sb.append(hexCode[(datum>>4)& 0xF]); sb.append(hexCode[datum& 0xF]); } return sb.toString(); } public static void main(String[] args) { generateValue(); } }
(5)
shiroRealm
:自定义realm,即通过该realm实现用户的认证及授权@Component public class ShiroRealm extends AuthorizingRealm { @Resource private ShiroService shiroService; @Override public boolean supports(AuthenticationToken token) { return token instanceof ShiroToken; } /** * @description:用户授权 <br> * @date: 19-10-9 上午11:43 */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { //获取用户信息 UserDO userDO = (UserDO) principals.getPrimaryPrincipal(); //获取用户权限列表 Set<String> userPermission = shiroService.getUserPermission(userDO.getUserId()); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); info.setStringPermissions(userPermission); return info; } /** * @description:用户认证 <br> * @date: 19-10-9 上午11:43 */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { //获取用户传入token String accessToken = (String) token.getPrincipal(); //根据token查询用户信息 UserTokenDO userTokenDO = shiroService.queryByToken(accessToken); if (Objects.isNull(userTokenDO) || userTokenDO.getExpireTime().getTime() < System .currentTimeMillis()) { throw new IncorrectCredentialsException("token失效,请重新登录"); } //根据用户id查询用户 UserDO userDO = shiroService.queryUser(userTokenDO.getUserId()); if(Objects.equals(userDO.getLocked(), 0)){ throw new LockedAccountException("账号被锁定,请联系管理员"); } SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(userDO, accessToken, getName()); return info; } }
(5)配置整体过滤器
@Configuration public class FilterConfig { @Bean public FilterRegistrationBean shiroFilterRegistration(){ FilterRegistrationBean registration = new FilterRegistrationBean(); registration.setFilter(new DelegatingFilterProxy("shiroFilter")); //该值缺省为false,表示生命周期由SpringApplicationContext管理,设置为true则表示由ServletContainer管理 registration.addInitParameter("targetFilterLifecycle", "true"); registration.setEnabled(true); registration.setOrder(Integer.MAX_VALUE - 1); registration.addUrlPatterns("/*"); return registration; } }
-
测试校验
(1)验证用户登录,创建token
//实体类 @Data public class LoginForm { private String username; private String password; private String uuid; } //请求controller @PostMapping("/sys/login") public Map<String, Object> login(@RequestBody LoginForm form) throws IOException { //校验验证码,省略 //账号密码校验 UserDO user = userService.queryByUserName(form.getUsername()); if (Objects.isNull(user) || !Objects .equals(new Sha256Hash(form.getPassword(), user.getSalt()).toHex(), user.getPassword())) { return ExtResult.error("账户或密码不正确"); } //账号锁定 if (Objects.equals(user.getLocked(), 0)) { return ExtResult.error("账号已被锁定,请联系管理员"); } //生成token,并入库 ExtResult r = userTokenService.createToken(user.getUserId()); return r; } } ----------------------------------------------------------------------------- //生成token主要逻辑 private final static int EXPIRE = 3600 * 12; @Override @Transactional public ExtResult createToken(long userId) { //生成一个token String token = TokenGenerator.generateValue(); Date now = new Date(); Date exprise = new Date(now.getTime() + EXPIRE * 1000); UserTokenDO userTokenDO = this.getById(userId); //如果token不存在,则生成新的token if(Objects.isNull(userTokenDO)){ userTokenDO = new UserTokenDO(); userTokenDO.setUserId(userId); userTokenDO.setToken(token); userTokenDO.setExpireTime(exprise); userTokenDO.setUpdateTime(now); this.saveOrUpdate(userTokenDO); } //如果存在,则更新token,并更新过期时间 else{ userTokenDO.setUpdateTime(now); userTokenDO.setExpireTime(exprise); userTokenDO.setToken(token); this.updateById(userTokenDO); } return ExtResult.ok().put("token", token).put("exprise", exprise); }
(2)新增用户
@PostMapping("/save") @RequiresPermissions("sys:user:save") public ExtResult save(@RequestBody UserDO user){ ValidatorUtils.validateEntity(user, AddGroup.class); user.setCreateUserId(getUserId()); //密码加盐 user.setPassword(MD5Utils.encrypt(user.getPassword(), user.getSalt())); userService.save(user); return ExtResult.ok(); } ----------------------------------------------------------------------------- // ValidatorUtils:用于校验传入参数 public static void validateEntity(Object object, Class<?>... groups) throws ExtException { Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups); if (!constraintViolations.isEmpty()) { StringBuilder msg = new StringBuilder(); for(ConstraintViolation<Object> constraint: constraintViolations){ msg.append(constraint.getMessage()).append("<br>"); } throw new ExtException(msg.toString()); } }
(3)实现步骤
- 用户登录根据用户名查询出用户信息,校验密码正确,校验账号状态是否正常
- 新建用户时,传入当前登录用户token,首先进入shiroRealm(doGetAuthenticationInfo)校验用户是否登录,token是否有效,如果校验成功进入下一步
- 上一步校验成功后,进入回调方法(doGetAuthorizationInfo),根据传入用户查询该用户权限列表,如果权限列表中包含@RequiresPermissions(“sys:user:save”),指定的权限,则继续执行后续操作。
3.参考资料
- 开源项目
renren-fast
- 跟我学shiro