简述
Apache Shiro是java的一个安全框架,Shiro可以帮助我们完成认证、授权、加密、会话管理、与Web集成、缓存等。而且Shiro的API也比较简单,这里我们就不进行过多的赘述,想要详细了解Shiro的,推荐看开涛的博客(点这里)
在Shiro的强大权限管理的基础上,我们实现单点登录就容易了很多,结合我上篇博客所讲的JSON Web Token(推荐先看这篇博客)就可以完成单点登录系统。
实现过程
在使用Shiro实现登录的时候,将登录成功的信息包括Token信息返回给前端,前端在请求后台时,将Token信息存入请求头中。配置自定义拦截器,拦截所有URL请求,取出请求头信息中的Token信息,对Token信息进行验证,对于redis中存在的登录时生成的Token信息,如果Token信息正确,则确认该用户已经登录,否则拒绝请求,返回401错误。
1.引入所需jar包
<!--json-web-token-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>
<!--shiro-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
</dependency>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
2.登录认证
要实现单点登录功能,首先要完成的就是登录功能,这里我们使用Shiro的认证来完成登录。
2.1 spring-shiro的配置文件
<!--配置SecurityManager-->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realms">
<list>
<ref bean="shiroRealm"/>
</list>
</property>
</bean>
<!--配置Realm-->
<!--直接配置实现了org.apache.shiro.realm.Realm接口的bean-->
<bean id="shiroRealm" class="com.why.authority.realms.ShiroRealm">
<!-- 凭证匹配器:配置登录验证所使用的加密算法-->
<property name="credentialsMatcher">
<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<property name="hashAlgorithmName" value="sha-512"/>
<property name="hashIterations" value="1024"/>
</bean>
</property>
</bean>
<!-- shiro拦截器-->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="/user/index"/>
<!--配置哪些页面需要受保护
1. anon 可以被匿名访问
2. authc 必须认证(登录)后才可能访问的页面
3. logout 登出
-->
<!--自定义filters,该拦截器即为实现单点登录的拦截器-->
<property name="filters">
<map>
<entry key="acf">
<bean class="com.why.authority.filter.AccessingControlFilter"/>
</entry>
</map>
</property>
<property name="filterChainDefinitions">
<value>
/user/index=anon
/user/login=anon
/user/content= acf
/** = acf
</value>
</property>
</bean>
- 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
2.2 登录方法
Controller:
@RequestMapping(value = {"/login"}, method = RequestMethod.POST)
@ResponseBody
public WhyResult content(@RequestParam("usercode") String usercode, @RequestParam("password") String password) {
String userInfoKey = "aum:user:" + usercode;
String tokenKey = "aum:token:"+usercode;
try {
if(StringUtils.isBlank(usercode) || StringUtils.isBlank(password)){
throw new UnknownAccountException();
}
//1. 执行登录
//把用户名和密码封装为UsernamePasswordToken对象
UsernamePasswordToken token = new UsernamePasswordToken(usercode, password);
SecurityUtils.getSubject().login(token);
//2.获取用户信息userEntity,redis中不存在则存入redis
UserEntity userEntity = new UserEntity();
//2.1 从redis中获取或从数据库中获取
String strUserInfo = JedisCacheUtil.get(userInfoKey);
if (!StringUtils.isBlank(strUserInfo)) {
userEntity = JacksonJsonUntil.jsonToPojo(strUserInfo, UserEntity.class);
} else {
userEntity = addUserInfoToRedis(usercode, userInfoKey);
}
//3.生成Token信息并保存到redis
LoginEntity loginEntity = addTokenToRedis(userEntity,tokenKey);
return WhyResult.build(200,"登录成功!",loginEntity);
//所有认证异常的父类
} catch (AuthenticationException e) {
logger.error("登录失败!",e);
return WhyResult.build(401,"用户名或密码错误!");
}
}
- 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
自定义Realm
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//1.把AuthenticationToken转换为UsernamePasswordToken
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
//2.从UsernamePasswordToken中获取userCode
String userCode = usernamePasswordToken.getUsername();
String userInfoKey = "aum:user:" + userCode;
UserEntity userEntity;
//3.获取用户信息userEntity
//3.1 从redis中获取
String strUserInfo;
try {
strUserInfo = JedisCacheUtil.get(userInfoKey);
if (!StringUtils.isBlank(strUserInfo)) {
userEntity = JacksonJsonUntil.jsonToPojo(strUserInfo, UserEntity.class);
} else {
userEntity = addUserAndGetUser(userCode, userInfoKey);
}
} catch (Exception e) {
userEntity = addUserAndGetUser(userCode, userInfoKey);
}
//6.根据用户的情况,来构建AuthenticationInfo对象并返回
String credentials = userEntity.getPassword();
//使用ByteSource.Util.bytes()来计算盐值
ByteSource credentialsSalt = ByteSource.Util.bytes(userCode);
return new SimpleAuthenticationInfo(userEntity, credentials, credentialsSalt, getName());
}
- 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
3. 自定义拦截器
该拦截器是在spring-shiro.xml文件中配置的自定义拦截器,原理就是拦截每个请求,验证URL请求头信息中的Token信息是否过期,是否被篡改。
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
//是否验证通过
boolean bool = false;
try {
HttpServletRequest req = WebUtils.toHttp(servletRequest);
String firstLoginToken = req.getParameter("token");
//从token中获得信息
Claims claims = TokenUtil.getClaims(firstLoginToken);
String userCode = claims.getSubject();
String userId = claims.getId();
String redisLoginKey = "aum:token:" + userCode;
String redisToken = JedisCacheUtil.get(redisLoginKey);
if(!StringUtils.isBlank(redisToken)){
String[] arrayRedisToken = redisToken.split("@");
//将用户传过来的token和redis中的做对比,若一样,认为已经登录
if (arrayRedisToken[0].equals(firstLoginToken)) {
//比较这次访问与登录的时间间隔有多少分钟,如果大于5分钟,则更新redis中的上次访问时间信息,将过期时间从新设定为30分钟
long diffMin = TokenUtil.CompareTime(arrayRedisToken[1]);
if (diffMin >= 5) {
String currentAccessTime = PasswordUtil.base64Encoede(String.valueOf(System.currentTimeMillis()));
//更新redis中的token登录信息
JedisCacheUtil.set(redisLoginKey, arrayRedisToken[0] + "@" + currentAccessTime, 30 * 60);
}
bool=true;
}
}
} catch (Exception e) {
return bool;
}
return bool;
}
- 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
至此为止,关键代码已经展示完了,现在实现的仅仅是最基础的单点登录,还需要进行更多的安全检查和验证,这里就不介绍了。