🔒 修复了一些已知的安全问题

🔒 修复了一些已知的安全问题
This commit is contained in:
ronger 2021-12-10 09:47:52 +08:00 committed by GitHub
commit 8e42504033
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 188 additions and 30 deletions

View File

@ -21,7 +21,7 @@ import java.util.Map;
@RequestMapping("/api/v1/answer")
public class AnswerController {
private final static String ANSWER_API_URL = "http://101.132.237.86:8089/question";
private final static String ANSWER_API_URL = "http://1.116.175.112:8089/question";
@GetMapping("/today")
public GlobalResult today() throws BaseApiException {

View File

@ -0,0 +1,54 @@
package com.rymcu.forest.config;
import com.rymcu.forest.entity.User;
import com.rymcu.forest.jwt.def.JwtConstants;
import com.rymcu.forest.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.listener.KeyExpirationEventMessageListener;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* Created on 2021/10/9 9:25.
*
* @author ronger
* @email ronger-x@outlook.com
* @packageName com.rymcu.forest.config
*/
@Slf4j
@Component
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {
@Resource
private UserService userService;
@Autowired
public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
super(listenerContainer);
}
/**
* 针对 redis 数据失效事件进行数据处理
*
* @param message key
* @param pattern pattern
*/
@Override
public void onMessage(Message message, byte[] pattern) {
// 获取到失效的 key
String expiredKey = message.toString();
if (expiredKey.contains(JwtConstants.LAST_ONLINE)) {
String email = expiredKey.replace(JwtConstants.LAST_ONLINE, "");
log.info("拿到过期的数据:{}", expiredKey);
log.info("处理后的数据:{}", email);
userService.updateLastOnlineTimeByEmail(email);
}
super.onMessage(message, pattern);
}
}

View File

@ -0,0 +1,24 @@
package com.rymcu.forest.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
/**
* Created on 2021/10/9 9:23.
*
* @author ronger
* @email ronger-x@outlook.com
* @packageName com.rymcu.forest.config
*/
@Configuration
public class RedisListenerConfig {
@Bean
public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
return container;
}
}

View File

@ -3,6 +3,7 @@ package com.rymcu.forest.dto;
import com.alibaba.fastjson.annotation.JSONField;
import lombok.Data;
import javax.persistence.Column;
import java.io.Serializable;
import java.util.Date;
@ -37,4 +38,12 @@ public class UserInfoDTO implements Serializable {
@JSONField(format = "yyyy-MM-dd HH:mm")
private Date lastLoginTime;
@JSONField(format = "yyyy-MM-dd HH:mm")
private Date lastOnlineTime;
@JSONField(format = "yyyy-MM-dd HH:mm:ss")
private Date createdTime;
private Integer onlineStatus;
}

View File

@ -108,9 +108,16 @@ public class User implements Serializable,Cloneable {
private Date createdTime;
/**
* 创建时间
* 更新时间
* */
@Column(name = "updated_time")
@JSONField(format = "yyyy-MM-dd HH:mm:ss")
private Date updatedTime;
/**
* 最后在线时间
* */
@Column(name = "last_online_time")
@JSONField(format = "yyyy-MM-dd HH:mm:ss")
private Date lastOnlineTime;
}

View File

@ -4,6 +4,7 @@ package com.rymcu.forest.jwt.aop;
import com.rymcu.forest.jwt.def.JwtConstants;
import com.rymcu.forest.jwt.model.TokenModel;
import com.rymcu.forest.jwt.service.TokenManager;
import com.rymcu.forest.mapper.UserMapper;
import com.rymcu.forest.web.api.exception.BaseApiException;
import com.rymcu.forest.web.api.exception.ErrorCode;
import io.jsonwebtoken.Claims;
@ -14,6 +15,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Objects;
@ -28,6 +30,8 @@ public class RestAuthTokenInterceptor implements HandlerInterceptor {
@Autowired
private TokenManager manager;
@Resource
private UserMapper userMapper;
@Override
public void afterCompletion(HttpServletRequest httpservletrequest, HttpServletResponse httpservletresponse, Object obj, Exception exception) throws Exception {
@ -36,7 +40,6 @@ public class RestAuthTokenInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object obj) throws Exception {
//从header中得到token
String authHeader = request.getHeader(JwtConstants.AUTHORIZATION);
if(StringUtils.isBlank(authHeader)){
@ -63,6 +66,19 @@ public class RestAuthTokenInterceptor implements HandlerInterceptor {
request.setAttribute(JwtConstants.CURRENT_TOKEN_CLAIMS, claims);
//如果token验证成功将token对应的用户id存在request中便于之后注入
request.setAttribute(JwtConstants.CURRENT_USER_NAME, model.getUsername());
// 判断是否为后台接口或财政划转接口
String adminApi = "/admin";
String transactionApi = "/transaction";
String uri = request.getRequestURI();
if (uri.contains(adminApi) || uri.contains(transactionApi)) {
// 判断管理员权限
boolean hasPermission = userMapper.hasAdminPermission(model.getUsername());
if (hasPermission) {
return true;
} else {
throw new BaseApiException(ErrorCode.ACCESS_DENIED);
}
}
return true;
} else {
throw new BaseApiException(ErrorCode.TOKEN_);

View File

@ -14,6 +14,8 @@ public class JwtConstants {
public static final String UPLOAD_TOKEN = "X-Upload-Token";
public static final String CURRENT_USER_NAME = "CURRENT_TOKEN_USER_NAME";
public static final String CURRENT_TOKEN_CLAIMS = "CURRENT_TOKEN_CLAIMS";
public static final String LAST_ONLINE = "last_online:";
public static final long TOKEN_EXPIRES_HOUR = 2 * 60;
public static final long LAST_ONLINE_EXPIRES_MINUTE = 10;
}

View File

@ -9,6 +9,8 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.concurrent.TimeUnit;
@ -50,6 +52,9 @@ public class RedisTokenManager implements TokenManager {
}
//如果验证成功说明此用户进行了一次有效操作延长token的过期时间
redisTemplate.boundValueOps(model.getUsername()).expire(JwtConstants.TOKEN_EXPIRES_HOUR, TimeUnit.HOURS);
StringBuilder key = new StringBuilder();
key.append(JwtConstants.LAST_ONLINE).append(model.getUsername());
redisTemplate.boundValueOps(key.toString()).set(LocalDateTime.now().toString(), JwtConstants.LAST_ONLINE_EXPIRES_MINUTE, TimeUnit.MINUTES);
return true;
}

View File

@ -95,13 +95,11 @@ public interface UserMapper extends Mapper<User> {
* @param nickname
* @param avatarType
* @param avatarUrl
* @param email
* @param phone
* @param signature
* @param sex
* @return
*/
Integer updateUserInfo(@Param("idUser") Integer idUser, @Param("nickname") String nickname, @Param("avatarType") String avatarType, @Param("avatarUrl") String avatarUrl, @Param("email") String email, @Param("phone") String phone, @Param("signature") String signature, @Param("sex") String sex);
Integer updateUserInfo(@Param("idUser") Integer idUser, @Param("nickname") String nickname, @Param("avatarType") String avatarType, @Param("avatarUrl") String avatarUrl, @Param("signature") String signature, @Param("sex") String sex);
/**
* 验证昵称是否重复
@ -145,5 +143,19 @@ public interface UserMapper extends Mapper<User> {
* @param searchDTO
* @return
*/
List<User> selectUsers(@Param("searchDTO") UserSearchDTO searchDTO);
List<UserInfoDTO> selectUsers(@Param("searchDTO") UserSearchDTO searchDTO);
/**
* 更新用户最后在线时间
* @param email
* @return
*/
Integer updateLastOnlineTimeByEmail(@Param("email") String email);
/**
* 判断用户是否拥有管理员权限
* @param email
* @return
*/
boolean hasAdminPermission(@Param("email") String email);
}

View File

@ -142,5 +142,12 @@ public interface UserService extends Service<User> {
* @param searchDTO
* @return
*/
List<User> findUsers(UserSearchDTO searchDTO);
List<UserInfoDTO> findUsers(UserSearchDTO searchDTO);
/**
* 通过邮箱查询用户信息
* @param email
* @return
*/
Integer updateLastOnlineTimeByEmail(String email);
}

View File

@ -6,6 +6,7 @@ import com.rymcu.forest.dto.*;
import com.rymcu.forest.entity.Role;
import com.rymcu.forest.entity.User;
import com.rymcu.forest.entity.UserExtend;
import com.rymcu.forest.jwt.def.JwtConstants;
import com.rymcu.forest.jwt.service.TokenManager;
import com.rymcu.forest.lucene.model.UserLucene;
import com.rymcu.forest.lucene.util.UserIndexUtil;
@ -198,8 +199,7 @@ public class UserServiceImpl extends AbstractService<User> implements UserServic
user.setAvatarUrl(avatarUrl);
user.setAvatarType("0");
}
Integer result = userMapper.updateUserInfo(user.getIdUser(), user.getNickname(), user.getAvatarType(),user.getAvatarUrl(),
user.getEmail(),user.getPhone(),user.getSignature(), user.getSex());
Integer result = userMapper.updateUserInfo(user.getIdUser(), user.getNickname(), user.getAvatarType(),user.getAvatarUrl(),user.getSignature(), user.getSex());
UserIndexUtil.addIndex(UserLucene.builder()
.idUser(user.getIdUser())
.nickname(user.getNickname())
@ -262,7 +262,7 @@ public class UserServiceImpl extends AbstractService<User> implements UserServic
String email = changeEmailDTO.getEmail();
String code = changeEmailDTO.getCode();
String vCode = redisService.get(email);
if(StringUtils.isNotBlank(vCode)){
if(StringUtils.isNotBlank(vCode) && StringUtils.isNotBlank(code)){
if(vCode.equals(code)){
userMapper.updateEmail(idUser, email);
map.put("message","更新成功!");
@ -282,7 +282,24 @@ public class UserServiceImpl extends AbstractService<User> implements UserServic
}
@Override
public List<User> findUsers(UserSearchDTO searchDTO) {
return userMapper.selectUsers(searchDTO);
public List<UserInfoDTO> findUsers(UserSearchDTO searchDTO) {
List<UserInfoDTO> users = userMapper.selectUsers(searchDTO);
users.forEach(user -> {
user.setOnlineStatus(getUserOnlineStatus(user.getEmail()));
});
return users;
}
private Integer getUserOnlineStatus(String email) {
String lastOnlineTime = redisService.get(JwtConstants.LAST_ONLINE + email);
if (StringUtils.isBlank(lastOnlineTime)) {
return 0;
}
return 1;
}
@Override
public Integer updateLastOnlineTimeByEmail(String email) {
return userMapper.updateLastOnlineTimeByEmail(email);
}
}

View File

@ -4,10 +4,7 @@ import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.rymcu.forest.core.result.GlobalResult;
import com.rymcu.forest.core.result.GlobalResultGenerator;
import com.rymcu.forest.dto.ArticleDTO;
import com.rymcu.forest.dto.ArticleSearchDTO;
import com.rymcu.forest.dto.CommentDTO;
import com.rymcu.forest.dto.UserSearchDTO;
import com.rymcu.forest.dto.*;
import com.rymcu.forest.dto.admin.TopicTagDTO;
import com.rymcu.forest.dto.admin.UserRoleDTO;
import com.rymcu.forest.entity.*;
@ -47,8 +44,8 @@ public class AdminController {
@GetMapping("/users")
public GlobalResult<Map<String, Object>> users(@RequestParam(defaultValue = "0") Integer page, @RequestParam(defaultValue = "10") Integer rows, UserSearchDTO searchDTO){
PageHelper.startPage(page, rows);
List<User> list = userService.findUsers(searchDTO);
PageInfo<User> pageInfo = new PageInfo<>(list);
List<UserInfoDTO> list = userService.findUsers(searchDTO);
PageInfo<UserInfoDTO> pageInfo = new PageInfo<>(list);
Map<String, Object> map = new HashMap<String, Object>(2);
map.put("users", pageInfo.getList());
Map pagination = Utils.getPagination(pageInfo);

View File

@ -2,10 +2,11 @@ package com.rymcu.forest.web.api.exception;
public enum ErrorCode {
UNAUTHORIZED(401, "请求要求用户的身份认证"),//未认证签名错误
UNAUTHORIZED(401, "请求要求用户的身份认证"),
INVALID_TOKEN(402, "TOKEN验证失败无效的TOKEN"),
TOKEN_(402, "TOKEN验证失败无效的TOKEN"),
NOT_FOUND(404, "此接口不存在"),//接口不存在
ACCESS_DENIED(403, "服务器拒绝请求!"),
NOT_FOUND(404, "此接口不存在"),
INTERNAL_SERVER_ERROR(500, "服务内部异常");
private int code;

View File

@ -17,6 +17,7 @@
<result column="phone" property="phone"/>
<result column="status" property="status"/>
<result column="last_login_time" property="lastLoginTime"/>
<result column="last_online_time" property="lastOnlineTime"/>
<result column="created_time" property="createdTime"/>
<result column="updated_time" property="updatedTime"/>
</resultMap>
@ -31,6 +32,8 @@
<result column="phone" property="phone"/>
<result column="status" property="status"/>
<result column="last_login_time" property="lastLoginTime"/>
<result column="last_online_time" property="lastOnlineTime"/>
<result column="created_time" property="createdTime"/>
<result column="signature" property="signature"/>
</resultMap>
<resultMap id="DTOResultMapper" type="com.rymcu.forest.dto.UserDTO">
@ -60,10 +63,7 @@
update forest_user set status = #{status} where id = #{idUser}
</update>
<update id="updateUserInfo">
update forest_user set nickname = #{nickname},email = #{email},signature = #{signature},avatar_type = #{avatarType},avatar_url = #{avatarUrl},sex = #{sex}
<if test="phone != null and phone != ''">
,phone = #{phone}
</if>
update forest_user set nickname = #{nickname},signature = #{signature},avatar_type = #{avatarType},avatar_url = #{avatarUrl},sex = #{sex}
where id = #{idUser}
</update>
<update id="updateLastLoginTime">
@ -75,12 +75,15 @@
<update id="updatePasswordById">
update forest_user set password = #{password} where id = #{idUser}
</update>
<update id="updateLastOnlineTimeByEmail">
update forest_user set last_online_time = sysdate() where email = #{email}
</update>
<select id="findByAccount" resultMap="BaseResultMap">
select id, nickname, account, password, status, avatar_type, avatar_url from forest_user where (account = #{account} or email = #{account} ) and status = 0
</select>
<select id="findUserInfoByAccount" resultMap="UserInfoResultMapper">
select id, nickname, sex, avatar_type, avatar_url, email, phone, account, status, signature, last_login_time from forest_user where account = #{account}
select id, nickname, sex, avatar_type, avatar_url, email, phone, account, status, signature, last_login_time, last_online_time from forest_user where account = #{account}
</select>
<select id="selectUserDTOByAccount" resultMap="DTOResultMapper">
select id, nickname, avatar_type, avatar_url, account, signature from forest_user where account = #{account} and status = 0
@ -92,7 +95,7 @@
select count(*) from forest_user where nickname = #{nickname}
</select>
<select id="selectUserInfo" resultMap="UserInfoResultMapper">
select id, nickname, sex, avatar_type, avatar_url, email, phone, account, status, signature, last_login_time from forest_user where id = #{idUser}
select id, nickname, sex, avatar_type, avatar_url, email, phone, account, status, signature, last_login_time, last_online_time from forest_user where id = #{idUser}
</select>
<select id="checkNicknameByIdUser" resultType="java.lang.Integer">
select count(*) from forest_user where nickname = #{nickname} and id != #{idUser}
@ -100,14 +103,18 @@
<select id="selectAuthor" resultMap="AuthorResultMap">
select * from forest_user where id = #{id}
</select>
<select id="selectUsers" resultMap="BaseResultMap">
select id, nickname, sex, avatar_type, avatar_url, email, account, status, last_login_time, created_time from forest_user
<select id="selectUsers" resultMap="UserInfoResultMapper">
select id, nickname, sex, avatar_type, avatar_url, email, account, status, last_login_time, created_time, last_online_time from forest_user
<where>
<if test="searchDTO.nickname != null and searchDTO.nickname != ''">
and instr(nickname, #{searchDTO.nickname}) > 0
</if>
</where>
order by last_login_time desc
order by last_online_time desc
</select>
<select id="hasAdminPermission" resultType="java.lang.Boolean">
select if(count(fur.id_role) = 0, false, true) from forest_user_role fur join forest_user fu on fur.id_user = fu.id
where fu.email = #{email} and exists(select id_role from forest_role fr where instr(fr.input_code, 'admin') > 0 and fr.id = fur.id_role)
</select>
</mapper>