shiro
三大组件 :
Subject:包含用户操作信息,
SecurityManager:shiro 的核心心脏,安全管理器,用户的登录校验
Realm:数据库用户信息,权限角色验证等
核心方法:
身份验证(getAuthenticationInfo 方法)验证账户和密码,并返回相关信息
权限获取(getAuthorizationInfo 方法) 获取指定身份的权限,并返回相关信息
令牌支持(supports方法)判断该令牌(Token)是否被支持,令牌有很多种类型,例如:HostAuthenticationToken(主机验证令牌),UsernamePasswordToken(账户密码验证令牌)
采用jwt+redis+shiro+springboot 通过 rbca 实现框架搭建
注意:
1. jwt本身就有超时时间为什么还用redis?jwt无法控制退出后清除token,导致退出后还能拿token登录,且没有请求延长token时效功能,所以交给redis控制,jwt仅作为工具使用redis存储方式 虽然违背了 jwt 使用原则,但是基于业务这是更好的选择。
2. shiro 认证AuthenticationInfo 和授权AuthorizationInfo 抛出异常如何通过全局异常处理返回给前端?全局异常无法捕捉到shiro的异常,只能通过自己的BasicHttpAuthenticationFilter 捕捉进行处理,例如: public class JwtFilter extends BasicHttpAuthenticationFilter{protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {try {........} catch (java.lang.Exception e) {this.response401(request,response,401,e.getCause().getMessage());}}......./*** 捕捉shiro 认证授权异常!!!,全局异常无法捕捉,只能从这里捕捉 全部 401*/private void response401(ServletRequest req, ServletResponse resp,int code,String msg) {try {HttpServletResponse response = (HttpServletResponse) resp;response.setHeader("Context-type", "application/json;charset=UTF-8");response.setCharacterEncoding("UTF-8");JSONObject jsonObject = new JSONObject();jsonObject.put("data","");jsonObject.put("code",code);jsonObject.put("msg",msg);PrintWriter out = response.getWriter();out.print(jsonObject.toJSONString());out.flush();out.close();} catch (java.lang.Exception e) {}}}
3. redis 可以在每次请求都给token重置超时,但在filter里无法注入redisTemplate?因为生命周期 filter --> servlet 所以在servlet之前,无法将redis注入到容器,只能写好工具类主动获取:RedisUtil redisUtil = SpringContextUtil.getBean(RedisUtil.class);
4. 每次请求都走 shiro 认证授权,角色权限都要查库,耗费资源,可以缓存到redis
结构
pomorg.springframework.bootspring-boot-starter-weborg.springframework.bootspring-boot-starter-testcom.alibabadruid-spring-boot-starter1.1.10mysqlmysql-connector-javaruntimecom.baomidoumybatis-plus-boot-starter3.3.2com.baomidoumybatis-plus-generator3.4.1org.apache.velocityvelocity-engine-core2.2org.apache.shiroshiro-core1.4.0org.apache.shiroshiro-spring1.4.0org.apache.shiroshiro-ehcache1.4.0com.auth0java-jwt3.2.0org.projectlomboklombokio.jsonwebtokenjjwt0.9.0io.springfoxspringfox-swagger22.9.2com.github.xiaoyminswagger-bootstrap-ui1.9.6commons-langcommons-lang2.6org.aspectjaspectjweaver1.9.7joda-timejoda-timeorg.apache.commonscommons-lang33.9org.springframework.bootspring-boot-starter-data-rediscom.alibabafastjson1.2.28
yml
server:port: 8080
spring:datasource:url: jdbc:mysql://localhost:3307/shiro?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTCusername: rootpassword: phptsdriver-class-name: com.mysql.jdbc.Driverjackson:time-zone: Asia/Shanghairedis:jedis:pool:#最大连接数max-active: 8#最大阻塞等待时间(负数表示没限制)max-wait: -1#最大空闲max-idle: 8#最小空闲min-idle: 0#连接超时时间timeout: 10000
token:# 在redis中有效时长(分)life: 10# 生成token 秘钥secret: 123
包configs -->mybatisplus
@Configuration
public class MybatisPlusConfig {/*** 分页插件*/@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor(){MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();//分页插件interceptor.addInnerInterceptor(new PaginationInnerInterceptor());//针对 update和 delete语句,作用: 阻止恶意的全表更新删除interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());return interceptor;}
}@Component
public class MyBatisPlusFillTime implements MetaObjectHandler {@Overridepublic void insertFill(MetaObject metaObject) {//插入时间this.setFieldValByName("createTime", new Date(), metaObject);this.setFieldValByName("updateTime", new Date(), metaObject);}@Overridepublic void updateFill(MetaObject metaObject) {//更新时间this.setFieldValByName("updateTime", new Date(), metaObject);}
}
---------------------------------
包configs -->RedisConfig
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {@Bean(name = "redisTemplate")public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){RedisTemplate redisTemplate = new RedisTemplate<>();//参照StringRedisTemplate内部实现指定序列化器redisTemplate.setConnectionFactory(redisConnectionFactory);redisTemplate.setKeySerializer(keySerializer());redisTemplate.setHashKeySerializer(keySerializer());redisTemplate.setValueSerializer(valueSerializer());redisTemplate.setHashValueSerializer(valueSerializer());return redisTemplate;}private RedisSerializer keySerializer(){return new StringRedisSerializer();}//使用Jackson序列化器private RedisSerializer
controller
@Api(tags = "登录管理")
@RestController
public class LoginController {@Resourceprivate LoginService loginService;@ApiOperation(value = "用户登录(账号互相挤模式)")@PostMapping("/sys/login")public R login(@RequestBody UserInfo userInfo){return loginService.login(userInfo);}@ApiOperation(value = "用户登录(账号共享模式)")@PostMapping("/sys/loginMany")public R loginMany(@RequestBody UserInfo userInfo){return loginService.loginMany(userInfo);}
}== servicepublic interface LoginService {/*** 登录成功并返回token* @return*/R login(UserInfo userInfo);/*** 多人登录成功并返回token* @return*/R loginMany(UserInfo userInfo);/*** 退出登录* @param userid* @return*/Object logout(Long userid);
}== impl @Service
public class LoginServiceImpl implements LoginService {//盐private final static String SALT = "123";@Autowiredprivate UserInfoService userService;@Autowiredprivate RedisUtil redisUtil;/*** 角色*/@Autowiredprivate RoleAuthService roleAuthService;/*** 权限*/@Autowiredprivate AuthInfoService authInfoService;/*** token 生命周期*/@Resourceprivate TokenLife tokenLife;@Overridepublic R login(UserInfo userInfo) {UserInfo user = userService.getOne(new QueryWrapper().eq("username", userInfo.getUsername()));//用户不存在if (user == null) {return R.fail("用户名或密码不正确!");}//密码 md5+盐String pwd = new SimpleHash("md5",userInfo.getPassword(),SALT,2).toHex();//校验密码是否相等if(!StringUtils.equals(pwd,user.getPassword())){return R.fail("用户名或密码不正确!");}//校验通过创建token 默认 12个小时超时(从内存中删除),但是控制时间还是在redis这,redis过期了 有token也没用了 得重新登陆String token = JwtTokenUtil.creatToken(user.getUsername(),tokenLife.getSecret());//删除redis中的tokenredisUtil.del(user.getUsername());//保存token 到redis,10min 超时redisUtil.set(user.getUsername(),token,tokenLife.getLife());user.setPassword(null);//获取登录用户角色权限信息,前端有可能需要Map roleAndPermission = this.getRoleAndPermission(user);return R.success(new HashMap(){{put("user", user);put("token", token);put("role", roleAndPermission.get("role"));put("permission", roleAndPermission.get("permission"));}});}@Overridepublic R loginMany(UserInfo userInfo) {UserInfo user = userService.getOne(new QueryWrapper().eq("username", userInfo.getUsername()));//用户不存在if (user == null) {return R.fail("用户名或密码不正确!");}//密码 md5+盐,经过两次hashString pwd = new SimpleHash("md5",userInfo.getPassword(),SALT,2).toHex();//校验密码是否相等if(!StringUtils.equals(pwd,user.getPassword())){return R.fail("用户名或密码不正确!");}//校验通过创建token 默认 12个小时超时(从内存中删除),但是控制时间还是在redis这,redis过期了 有token也没用了 得重新登陆String token = null;//多人共享账号,redis存在就获取,不存在就创建if(redisUtil.hasKey(user.getUsername())){token = redisUtil.get(user.getUsername()).toString();}else {token = JwtTokenUtil.creatToken(user.getUsername(),tokenLife.getSecret());//保存token 到redis,10min 超时redisUtil.set(user.getUsername(),token,tokenLife.getLife());}String finalToken = token;user.setPassword(null);//获取登录用户角色权限信息,前端有可能需要Map roleAndPermission = this.getRoleAndPermission(user);return R.success(new HashMap(){{put("user", user);put("token", finalToken);put("role", roleAndPermission.get("role"));put("permission", roleAndPermission.get("permission"));}});}@Overridepublic Object logout(Long userid) {return null;}/*** 登录获取权限角色一并返给前端*/public Map getRoleAndPermission(UserInfo user) {//1 存放角色权限Map info = new HashMap<>();//2 根据用户获取角色信息Integer roleId = user.getRoleId();Set roles = new HashSet<>();roles.add(String.valueOf(roleId));//3 根据角色信息获取权限信息Set authodKeys = new HashSet<>();// 3.1 去角色权限信息关联表去查所有的角色权限List allAuthorList = roleAuthService.list(new QueryWrapper().eq("role_id", roleId));if(CollectionUtils.isNotEmpty(allAuthorList)){// 3.2 去重的权限idList distinctAuthorId = allAuthorList.stream().map(e -> e.getAuthId()).collect(Collectors.toList());// 3.3 所有的权限信息List authInfoList = authInfoService.list(new QueryWrapper().in("id",distinctAuthorId ));if(CollectionUtils.isNotEmpty(authInfoList)){// 3.4 提取权限key: auth_keyauthodKeys = authInfoList.stream().map(e -> e.getAuthKey()).collect(Collectors.toSet());}}//4 角色列表info.put("role",roles);//5 权限列表info.put("permission",authodKeys);return info;}
}