springboot集成shiro-redis
springboot集成shiro-redis
近期公司项目中需要用到 shrio做登录认证,认证的token用redis存储,在集成过程中出现了一些问题,写这篇博客就是为了以后在调用的时候避坑,也希望可以帮助到其他遇到同样的问题。
项目架构
项目是前后端分离的项目,主要运用到以下的技术:
后端+后台管理:
- springboot;
- mybatise;
- maven;
- shrio;
- redis;
- angular.js;
- jdk1.8
- mysql;
- 阿里云 oss存储(视频和图片);
前端:
- uni app (一套代码多端运行);
- 微信 js-sdk(分享功能);
redis安装
公司用的是阿里云的服务器,系统是 CentOS 6.8 64位,使用putty+winSCP工具操作和上传,安装流程如下:
1、使用putty登录服务器 (这里方便操作,我直接使用虚拟机的centos 7,安装流程是不变的)
2、切换到 /usr/local目录下
3、在local下新建一个redis目录
4、使用上传工具WinSCP将redis的包上传到redis目录下(我的redis的安装包是提前下载好的,也可以在线安装)
5、解压redis tar -zxvf 解压包名
解压成功后,redis目录下会多出一个redis解压后的文件
6、在redis目录下创建两个目录,目录名随意,我这里用 bin和 config 表示
7、 安装 gcc 环境
redis是由C语言编写的,它的运行需要C环境,所以编译前需安装 gcc (安装过程比较长)
yum install gcc-c++ 遇到[y / d / n] 输入 y
8、安装完成进入 redis-5.0.5下 执行编译 make
9、进入到src目录下,执行 make install
编译工作已经完成。
10、切换到redis-5.0.5目录下,将redis-conf文件移动到config目录下
11、将redis-5.0.5 下 src下的绿色文件全部移动到 redis目录下的bin中
mv mkreleasehdr.sh redis-benchmark redis-check-aof redis-check-rdb redis-cli redis-sentinel redis-server redis-trib.rb /usr/local/redis/bin/
12、修改配置文件 (config/redis-conf)
1)、 将 bind 127.0.0.1 注释掉 或 将127.0.0.1改成0.0.0.0
2)、将 yes 改为 no
3)、将 daemonize 属性改为 yes (这样启动时就在后台启动)
4)、设置密码 随意设置=====建议设置密码(注意设置密码后很可能在集成时会入坑,后面有介绍)
密码:testredis
:wq 保存退出
13、启动redis
./bin/redis-server config/redis.conf
14、连接redis
出现如下界面说明已经连接
连接失败的情况: (本人直接在阿里云上操作的,入了一点小坑)
按照我给出的修改配置文件的步骤,99%是没有问题的,但是,虚拟机连接不上需要关闭防火墙即可。
如果是阿里云服务器:
是需要在控制台设置安全组规则的,开放6379端口。
本人入坑记录来啦:
所有的配置和安全组开放后,一直不能连上,最后查看防火墙的开放端口,服务器防火墙并没有开放6379端口,我也很迷茫为什么没有开放改端口,正常来说 安全组开放端口后都是可以的。最后解决办法:
查看tcp端口:
这里虽然是有6379端口,在防火墙的配置文件可能是不存在的。
编辑防火墙的配置文件:
vi /etc/sysconfig/iptables #编辑防火墙配置文件
在文件中添加
-A INPUT -m state --state NEW -m tcp -p tcp --dport 6379 -j ACCEPT
:wq #保存并退出
service iptables start #开启
这时在连接,完美 ok。
redis安装成功后、直接上集成的代码
一、导包,在pom/xml中导入 shrio和 shrio-redis的包
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.1.0</version>
</dependency>
<!-- redis缓存-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>
注意:这里的redis不用在导入jedis的包,这里的redis直接使用
@Autowired
private RedisTemplate redisTemplate;
springboot中集成shiro相对简单,只需要两个类:一个是shiroConfig类,一个是CustonRealm类,如果用redis相对发杂一些。
二、创建自定义realm 类
自定义的CustomRealm继承AuthorizingRealm。并且重写父类中的doGetAuthorizationInfo(权限相关)、doGetAuthenticationInfo(身份认证)这两个方法。
/**
* 自定义realm
*/
public class CustomRealm extends AuthorizingRealm {
@Autowired
private AdminService aminService;
public void setName(String name){
super.setName("customRealm");
}
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
return authorizationInfo;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("==========认证===========");
//获取登录信息
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
//获取password
char[] pwd = token.getPassword();
//将password转成string
String password=new String(pwd);
System.out.println("=======token.getUsername========="+token.getUsername());
System.out.println("=======token.password========="+password);
//查询数据库
Admin admin = aminService.adminLogin(token.getUsername());
//判断用户名和密码是否一致
if(admin!=null && admin.getPwd().equals(password)){
//如果一致返回安全数据
/**
* 参数一:安全数据
* 参数二:密码
* 参数三:realm域名
*/
SimpleAuthenticationInfo simpleAuthenticationInfo=new SimpleAuthenticationInfo(admin,admin.getPwd(),this.getName());
return simpleAuthenticationInfo;
}
return null;
}
}
注意:很容易入坑,char[] pwd = token.getPassword(); 这里必须是char[]。
之前我的是 String pwd = token.getPassword(); 然后一直报这行错误,然后检查代码没什么毛病,因为在controller中我的pwd就是String类型的。
然后寻求源代码的帮助,查看到了这样的代码。
是不是瞬间清爽啦。
三、自定义的sessionManager
public class CustomSessionManager extends DefaultWebSessionManager implements WebSessionManager {
private final String X_AUTH_TOKEN = "x-auth-token";
@Override
protected void onStart(Session session, SessionContext context) {
if (!WebUtils.isHttp(context)) {
} else {
HttpServletRequest request = WebUtils.getHttpRequest(context);
HttpServletResponse response = WebUtils.getHttpResponse(context);
Serializable sessionId = session.getId();
this.storeSessionId(sessionId, request, response);
request.removeAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_IS_NEW, Boolean.TRUE);
}
super.onStart(session, context);
}
/**
* 把sessionId 放入 response header 中
* onStart时调用
* 没有sessionid时 会产生sessionid 并放入 response header中
*/
private void storeSessionId(Serializable currentId, HttpServletRequest ignored, HttpServletResponse response) {
if (currentId == null) {
String msg = "sessionId cannot be null when persisting for subsequent requests.";
throw new IllegalArgumentException(msg);
} else {
String idString = currentId.toString();
response.setHeader(this.X_AUTH_TOKEN, idString);
System.out.println("======idString======"+idString);
}
}
/**
* 头信息中具有sessionid
* 请求头:Authorization : sessionId
* 指定seesionid获取方式
*/
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
//获取请求头中的Authorization的数据
String id=WebUtils.toHttp(request).getHeader("x-auth-token");
if(StringUtils.isEmpty(id)){
//如果没有携带,生成新的sessionid 从cookie中获取
System.out.println("前端传来的sessid="+id);
return super.getSessionId(request,response);
}else {
//返回sessionId 从header中获取
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, "header");
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return id;
}
}
}
四、shrio配置类
/**
* shrio配置类
* 1.实现 ShiroFilterFactoryBean
* 2.DefaultWebSecurityManager或SecurityManager
* 3.CustomRealm 自定义
*/
@Configuration
public class ShrioConfig {
//redis相关配置
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
@Value("${spring.redis.password}")
private String password;
//实现shiroFilterFactoryBean
@Bean
public ShiroFilterFactoryBean shiroFilterFactory(){
//创建ShiroFilterFactoryBean
ShiroFilterFactoryBean shiroFilterFactoryBean=new ShiroFilterFactoryBean();
//设置管理器
shiroFilterFactoryBean.setSecurityManager(webSecurityManager());
shiroFilterFactoryBean.setUnauthorizedUrl("/login/unauth");
shiroFilterFactoryBean.setLoginUrl("/login/unauth");
System.out.println("=========shiroFilterFactory==================");
//设置自定义认证
Map<String, String> filterChainDefinitionMap=new LinkedHashMap<>();
//配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了,登出后跳转配置的loginUrl
filterChainDefinitionMap.put("/logout", "logout");
// 配置不会被拦截的链接 顺序判断,在 ShiroConfiguration 中的 shiroFilter 处配置了 /ajaxLogin=anon,意味着可以不需要认证也可以访问
filterChainDefinitionMap.put("/login/login", "anon");
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
//2.DefaultWebSecurityManager
@Bean
public DefaultWebSecurityManager webSecurityManager(){
DefaultWebSecurityManager defaultWebSecurityManager=new DefaultWebSecurityManager();
defaultWebSecurityManager.setRealm(customRealm());
//将自定义的会话管理器注册到安全管理器
defaultWebSecurityManager.setSessionManager(this.sessionManager());
//将 自定义的redis缓存管理器注册到安全管理器
defaultWebSecurityManager.setCacheManager(this.cacheManager());
return defaultWebSecurityManager;
}
//3.
@Bean
public CustomRealm customRealm(){
return new CustomRealm();
}
/**
* 1.redis控制器,操作redis
*/
public RedisManager redisManager(){
RedisManager redisManager=new RedisManager();
redisManager.setHost(host);
redisManager.setPort(port);
redisManager.setPassword(password);
return redisManager;
}
/**
* 2.sessionDao
*/
public RedisSessionDAO sessionDAO(){
RedisSessionDAO sessionDAO=new RedisSessionDAO();
sessionDAO.setRedisManager(this.redisManager());
return sessionDAO;
}
/**
* 3.自定义会话管理
*/
public DefaultWebSessionManager sessionManager(){
CustomSessionManager sessionManager=new CustomSessionManager();
sessionManager.setSessionDAO(this.sessionDAO());
return sessionManager;
}
/**
* 缓存管理器
*/
public RedisCacheManager cacheManager(){
RedisCacheManager redisCacheManager=new RedisCacheManager();
redisCacheManager.setRedisManager(this.redisManager());
return redisCacheManager;
}
}
注意这里也是很容易入坑的,如果redis设置了密码,并且在属性文件中也定义了,这里一定要把password读取出来,否则调用时会报异常的。
本人之前就是没有添加这段代码,导致入坑了,写了测试代码是完全可以连接redis的,看了代码才知道这里少了密码,必须注意虽然读取出来,还得需要在以下代码中设置密码。
/**
* 1.redis控制器,操作redis
*/
public RedisManager redisManager(){
RedisManager redisManager=new RedisManager();
redisManager.setHost(host);
redisManager.setPort(port);
redisManager.setPassword(password);
return redisManager;
}
到这里shrio的基本配置以及完成,直接上代码业务:
//登录
@Controller
@RequestMapping("/login")
public class LoginController {
private static final Logger logger= LoggerFactory.getLogger(LoginController.class);
//执行登陆
@PostMapping("/login")
@ResponseBody
public Result login(@RequestParam(value = "tel") String tel,@RequestParam(value = "pwd") String pwd) {
logger.info("========初始密码=========="+pwd);
try {
/*pwd
shrio提供的md5
密码加密
参数一:加密内容
参数二:盐(用登录用户名)
参数三:加密次数
*/
pwd= new Md5Hash(pwd,tel,3).toString();
//获取token
UsernamePasswordToken token = new UsernamePasswordToken(tel, pwd);
//获取主体
Subject subject = SecurityUtils.getSubject();
System.out.println("=========密码========="+pwd);
logger.info("========加密密码=========="+pwd);
//认证成功
subject.login(token);
//获取sesssion
String sid=(String) subject.getSession().getId();
return new Result(true, "登录成功",sid);
} catch (IncorrectCredentialsException e) {
e.printStackTrace();
return new Result(false, "密码错误",null);
} catch (AuthenticationException e) {
e.printStackTrace();
return new Result(false, "用户不存在",null);
}
}
这里用到了给密码加密的方法,需要注意的是在插入的代码块中也是需要对密码进行加密加盐的。
加盐直接使用 password+username的方式进行加盐处理的。
到此 整合基本完成了。
测试: 未登录
登录后:
以上数据均为测试数据。
总结:shrio框架并不是很难,首先的理解它的执行原理以及它的核心,这样就会很快上手啦。如有遇到相关问题的同行,欢迎打扰。