赞
踩
该博文不做原理的讲解,直接提供代码,实现SpringBoot整合SpringSecurity+JWT+Redis前后分离。
如果不会搭建springboot项目,可以参考我下面写的博文,如果会搭建springboot项目,直接跳过这步。
第一节:Idea父子项目创建
第二节:springboot整合Mybatis(入门)
第三节:springboot整合Mybatis(mapper的@Select)
第四节:springboot整合Mybatis(controller+service+mapper)完整过程
第五节:springboot整合Mybatis(声明式事务@Transactional)
核心依赖
<!--security 依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!--redis 依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!--引入jwt--> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.11.0</version> </dependency> <!-- fastjson --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.83</version> </dependency>
其他依赖
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <!--单元测试--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--简化get set等--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <!-- https://mvnrepository.com/artifact/org.yaml/snakeyaml --> <dependency> <groupId>org.yaml</groupId> <artifactId>snakeyaml</artifactId> <version>1.25</version> </dependency> <!-- mybatis 支持 SprigBoot --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.3</version> </dependency> <!-- mysql 驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.38</version> </dependency> <!--阿里巴巴数据源--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.20</version> </dependency> <!--MyBatis Plus 的依赖包 ,比如可以直接使用他封装好的sql语句,selectOne等--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.1.2</version> </dependency> <!--常用工具类 比如数字处理NumberUtils、字符串处理类StringUtils、日期类DateUtils--> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.7</version> </dependency> </dependencies>
security没有啥配置,主要是配置数据库、redis。mysql和redis就不做安装的教程了,自己去安装下就好,很简单的。
server: port: 8081 spring: profiles: active: dev datasource: url: jdbc:mysql://localhost:3306/study-demo?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=false username: root password: root driver-class-name: com.mysql.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource #Spring Boot 默认是不注入这些属性值的,需要自己绑定 #druid 数据源专有配置 initialSize: 5 minIdle: 5 maxActive: 20 maxWait: 60000 timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000 validationQuery: SELECT 1 FROM DUAL testWhileIdle: true testOnBorrow: false testOnReturn: false poolPreparedStatements: true # redis配置 redis: host: localhost port: 6379 #我没有给redis设置账号和密码 #username: guest #password: guest # 日志输出配置 logging: level: root: info #jwt 自定义配置 Jwt: #jwt签名私钥 secretKey: sdsdsd23232323
看到输出Using generated security password:xxxx 就是Springboot整合SpringSecurity成功了
访问服务http://127.0.0.1:8081/ 这是我自己的ip和端口。你会看到跳转到SpringSecurity自带的页面了
我为了方便,就只建了用户表、权限表和用户权限关联表
CREATE TABLE `sys_user` (
`user_id` int(8) NOT NULL AUTO_INCREMENT,
`account` varchar(32) DEFAULT NULL COMMENT '账号',
`user_name` varchar(32) DEFAULT NULL COMMENT '用户名',
`password` varchar(64) DEFAULT NULL COMMENT '用户密码',
`last_login_time` datetime DEFAULT NULL COMMENT '上一次登录时间',
`enabled` tinyint(1) DEFAULT '1' COMMENT '账号是否可用。默认为1(可用)',
`account_not_expired` tinyint(1) DEFAULT '1' COMMENT '是否过期。默认为1(没有过期)',
`account_not_locked` tinyint(1) DEFAULT '1' COMMENT '账号是否锁定。默认为1(没有锁定)',
`credentials_not_expired` tinyint(1) DEFAULT NULL COMMENT '证书(密码)是否过期。默认为1(没有过期)',
`create_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`user_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='用户表';
CREATE TABLE `sys_permission` (
`permission_id` int(8) NOT NULL,
`permission_code` varchar(32) DEFAULT NULL,
`permission_name` varchar(32) DEFAULT NULL,
`url` varchar(255) DEFAULT NULL,
PRIMARY KEY (`permission_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='权限表';
CREATE TABLE `sys_user_permission_relation` (
`user_permission_relation_id` int(8) NOT NULL,
`user_id` int(8) DEFAULT NULL,
`permission_id` int(8) DEFAULT NULL,
PRIMARY KEY (`user_permission_relation_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户权限关联表';
INSERT INTO `sys_user` VALUES (1, '888', '小张', '$2a$10$2mO7/KcswzO3SQU7TX3fiOfkypjdOn3tLBezV/tf2IJXdQu1BpxK2', '2023-08-16 09:45:53', 1, 1, 1, 1, '2023-08-09 17:49:20', '2023-08-09 17:49:22');
INSERT INTO `sys_permission` VALUES (1, 'sys:queryUser', '查询用户', '/getUser');
INSERT INTO `sys_user_permission_relation` VALUES (1, 1, 1);
用户实体类
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import lombok.experimental.Accessors; import java.io.Serializable; import java.util.Date; @Data @Accessors(chain = true)//链式; 存取器。通过该注解可以控制getter和setter方法的形式。 @TableName("sys_user") public class SysUser implements Serializable { private static final long serialVersionUID = 915478504870211231L; @TableId(value = "user_id", type = IdType.ID_WORKER) private Integer userId; //账号 private String account; //用户名 private String userName; //用户密码 private String password; //上一次登录时间 private Date lastLoginTime; //账号是否可用。默认为1(可用) private Boolean enabled; //是否过期。默认为1(没有过期) private Boolean accountNotExpired; //账号是否锁定。默认为1(没有锁定) private Boolean accountNotLocked; //证书(密码)是否过期。默认为1(没有过期) private Boolean credentialsNotExpired; //创建时间 private Date createTime; //修改时间 private Date updateTime; }
用户mapper
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.study.test.web.entity.SysUser;//这是你自己实体类放的路径,记得修改下
public interface SysUserMapper extends BaseMapper<SysUser> {
}
权限实体类
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import lombok.experimental.Accessors; import java.io.Serializable; @Data @Accessors(chain = true)//链式; 存取器。通过该注解可以控制getter和setter方法的形式。 @TableName("sys_permission") public class SysPermission implements Serializable { @TableId(value = "permission_id", type = IdType.ID_WORKER) private Integer permissionId; private String permissionCode; private String permissionName; private String url; }
权限mapper
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.study.test.web.entity.SysPermission;//这是你自己实体类放的路径,记得修改下 import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import java.util.List; public interface SysPermissionMapper extends BaseMapper<SysPermission> { /** * 通过用户id查询用户的权限数据 * @param userId * @return */ @Select({"<script>"+ " SELECT p.* FROM"+ " sys_user u"+ " LEFT JOIN sys_user_permission_relation r ON u.user_id = r.user_id"+ " LEFT JOIN sys_permission p on r.permission_id = p.permission_id"+ " WHERE u.user_id = #{userId}"+ "</script>" }) List<SysPermission> selectPermissionList(@Param("userId") Integer userId); }
用户与权限关联实体类
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import lombok.experimental.Accessors; @Data @Accessors(chain = true)//链式; 存取器。通过该注解可以控制getter和setter方法的形式。 @TableName("sys_user_permission_relation") public class SysUserPermissionRelation { @TableId(value = "user_permission_relation_id", type = IdType.ID_WORKER) private Integer userPermissionRelationId; private Integer userId; private Integer permissionId; }
用户与权限关联mapper
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.study.test.web.entity.SysUserPermissionRelation;//这是你自己实体类放的路径,记得修改下
public interface SysUserPermissionRelationMapper extends BaseMapper<SysUserPermissionRelation> {
}
千万别忘记配置@MapperScan,不然找不到mapper,StudyApplication 是我启动类的名称。
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("com.study.test.web.mapper")//这要修改为你自己mapper放的路径
public class StudyApplication {
public static void main(String[] args) {
SpringApplication.run(StudyApplication.class, args);
}
}
这些工作完成后,记得重启下项目看下有没有成功,不成功就需要自己调试下了
这里的redis主要是为了实现SpringSecurity整合JWT,实现redis的token登录。
package com.study.test.common.config;//这是我存放redisconfig的路径 import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; //RedisConfig 配置类 @Configuration public class RedisConfig { //解决redis可视化乱码问题,方便调试查找问题 public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(connectionFactory); StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); redisTemplate.setKeySerializer(stringRedisSerializer); // key的序列化类型 Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper objectMapper = new ObjectMapper(); objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance , ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY); jackson2JsonRedisSerializer.setObjectMapper(objectMapper); jackson2JsonRedisSerializer.setObjectMapper(objectMapper); redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); // value的序列化类型 redisTemplate.setHashKeySerializer(stringRedisSerializer); redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer); redisTemplate.afterPropertiesSet(); return redisTemplate; } }
redis工具,解决使用redis代码重复问题
package com.study.test.common.utils;//这是我存放redis工具的路径 import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.annotation.Order; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; @Component @Order(-1) public final class RedisUtil { @Autowired private RedisTemplate redisTemplate; /** * 指定缓存失效时间 * * @param key 键 * @param time 时间(秒) * @return 0 */ public boolean expire(String key, long time) { try { if (time > 0) { redisTemplate.expire(key, time, TimeUnit.SECONDS); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 根据key 获取过期时间 * * @param key 键 不能为null * @return 时间(秒) 返回0代表为永久有效 */ public long getExpire(String key) { return redisTemplate.getExpire(key, TimeUnit.SECONDS); } /** * 判断key是否存在 * * @param key 键 * @return true 存在 false不存在 */ public boolean hasKey(String key) { try { return redisTemplate.hasKey(key); } catch (Exception e) { e.printStackTrace(); return false; } } /** * 删除缓存 * * @param key 可以传一个值 或多个 */ @SuppressWarnings("unchecked") public void del(String... key) { if (key != null && key.length > 0) { if (key.length == 1) { redisTemplate.delete(key[0]); } else { redisTemplate.delete(CollectionUtils.arrayToList(key)); } } } // ============================String============================= /** * 普通缓存获取 * * @param key 键 * @return 值 */ public Object get(String key) { return key == null ? null : redisTemplate.opsForValue().get(key); } /** * 普通缓存放入 * * @param key 键 * @param value 值 * @return true成功 false失败 */ public boolean set(String key, Object value) { try { redisTemplate.opsForValue().set(key, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 普通缓存放入并设置时间 * * @param key 键 * @param value 值 * @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期 * @return true成功 false 失败 */ public boolean set(String key, Object value, long time) { try { if (time > 0) { redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS); } else { set(key, value); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 递增 * * @param key 键 * @param delta 要增加几(大于0) * @return */ public long incr(String key, long delta) { if (delta < 0) { throw new RuntimeException("递增因子必须大于0"); } return redisTemplate.opsForValue().increment(key, delta); } /** * 递减 * * @param key 键 * @param delta 要减少几(小于0) * @return */ public long decr(String key, long delta) { if (delta < 0) { throw new RuntimeException("递减因子必须大于0"); } return redisTemplate.opsForValue().increment(key, -delta); } // ================================Map================================= /** * HashGet * * @param key 键 不能为null * @param item 项 不能为null * @return 值 */ public Object hget(String key, String item) { return redisTemplate.opsForHash().get(key, item); } /** * 获取hashKey对应的所有键值 * * @param key 键 * @return 对应的多个键值 */ public Map<Object, Object> hmget(String key) { return redisTemplate.opsForHash().entries(key); } /** * HashSet * * @param key 键 * @param map 对应多个键值 * @return true 成功 false 失败 */ public boolean hmset(String key, Map<String, Object> map) { try { redisTemplate.opsForHash().putAll(key, map); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * HashSet 并设置时间 * * @param key 键 * @param map 对应多个键值 * @param time 时间(秒) * @return true成功 false失败 */ public boolean hmset(String key, Map<String, Object> map, long time) { try { redisTemplate.opsForHash().putAll(key, map); if (time > 0) { expire(key, time); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 向一张hash表中放入数据,如果不存在将创建 * * @param key 键 * @param item 项 * @param value 值 * @return true 成功 false失败 */ public boolean hset(String key, String item, Object value) { try { redisTemplate.opsForHash().put(key, item, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 向一张hash表中放入数据,如果不存在将创建 * 0 * * @param key 键 * @param item 项 * @param value 值 * @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间 * @return true 成功 false失败 */ public boolean hset(String key, String item, Object value, long time) { try { redisTemplate.opsForHash().put(key, item, value); if (time > 0) { expire(key, time); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 删除hash表中的值 * * @param key 键 不能为null * @param item 项 可以使多个 不能为null */ public void hdel(String key, Object... item) { redisTemplate.opsForHash().delete(key, item); } /** * 判断hash表中是否有该项的值 * * @param key 键 不能为null * @param item 项 不能为null * @return true 存在 false不存在 */ public boolean hHasKey(String key, String item) { return redisTemplate.opsForHash().hasKey(key, item); } /** * hash递增 如果不存在,就会创建一个 并把新增后的值返回 * * @param key 键 * @param item 项 * @param by 要增加几(大于0) * @return */ public double hincr(String key, String item, double by) { return redisTemplate.opsForHash().increment(key, item, by); } /** * hash递减 * * @param key 键 * @param item 项 * @param by 要减少记(小于0) * @return */ public double hdecr(String key, String item, double by) { return redisTemplate.opsForHash().increment(key, item, -by); } // ============================set============================= /** * 根据key获取Set中的所有值 * * @param key 键 * @return */ public Set<Object> sGet(String key) { try { return redisTemplate.opsForSet().members(key); } catch (Exception e) { e.printStackTrace(); return null; } } /** * 根据value从一个set中查询,是否存在 * * @param key 键 * @param value 值 * @return true 存在 false不存在 */ public boolean sHasKey(String key, Object value) { try { return redisTemplate.opsForSet().isMember(key, value); } catch (Exception e) { e.printStackTrace(); return false; } } /** * 将数据放入set缓存 * * @param key 键 * @param values 值 可以是多个 * @return 成功个数 */ public long sSet(String key, Object... values) { try { return redisTemplate.opsForSet().add(key, values); } catch (Exception e) { e.printStackTrace(); return 0; } } /** * 将set数据放入缓存 * * @param key 键 * @param time 时间(秒) * @param values 值 可以是多个 * @return 成功个数 */ public long sSetAndTime(String key, long time, Object... values) { try { Long count = redisTemplate.opsForSet().add(key, values); if (time > 0) expire(key, time); return count; } catch (Exception e) { e.printStackTrace(); return 0; } } /** * 获取set缓存的长度 * * @param key 键 * @return */ public long sGetSetSize(String key) { try { return redisTemplate.opsForSet().size(key); } catch (Exception e) { e.printStackTrace(); return 0; } } /** * 移除值为value的 * * @param key 键 * @param values 值 可以是多个 * @return 移除的个数 */ public long setRemove(String key, Object... values) { try { Long count = redisTemplate.opsForSet().remove(key, values); return count; } catch (Exception e) { e.printStackTrace(); return 0; } } // ===============================list================================= /** * 获取list缓存的内容 * * @param key 键 * @param start 开始 * @param end 结束 0 到 -代表所有值 * @return */ public List<Object> lGet(String key, long start, long end) { try { return redisTemplate.opsForList().range(key, start, end); } catch (Exception e) { e.printStackTrace(); return null; } } /** * 获取list缓存的长度 * * @param key 键 * @return 0 */ public long lGetListSize(String key) { try { return redisTemplate.opsForList().size(key); } catch (Exception e) { e.printStackTrace(); return 0; } } /** * 通过索引 获取list中的值 * * @param key 键 * @param index 索引 index>=0时, 0 表头, 第二个元素,依次类推;index<0时,-,表尾,-倒数第二个元素,依次类推 * @return 0 */ public Object lGetIndex(String key, long index) { try { return redisTemplate.opsForList().index(key, index); } catch (Exception e) { e.printStackTrace(); return null; } } /** * 将list放入缓存 * * @param key 键 * @param value 值 * @return */ public boolean lSet(String key, Object value) { try { redisTemplate.opsForList().rightPush(key, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 将list放入缓存 * * @param key 键 * @param value 值 * @param time 时间(秒) * @return */ public boolean lSet(String key, Object value, long time) { try { redisTemplate.opsForList().rightPush(key, value); if (time > 0) expire(key, time); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 将list放入缓存 * * @param key 键 * @param value 值 * @return */ public boolean lSet(String key, List<Object> value) { try { redisTemplate.opsForList().rightPushAll(key, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 将list放入缓存 * * @param key 键 * @param value 值 * @param time 时间(秒) * @return 0 */ public boolean lSet(String key, List<Object> value, long time) { try { redisTemplate.opsForList().rightPushAll(key, value); if (time > 0) expire(key, time); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 根据索引修改list中的某条数据 * * @param key 键 * @param index 索引 * @param value 值 * @return 0 */ public boolean lUpdateIndex(String key, long index, Object value) { try { redisTemplate.opsForList().set(key, index, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 移除N个值为value * * @param key 键 * @param count 移除多少个 * @param value 值 * @return 移除的个数 */ public long lRemove(String key, long count, Object value) { try { Long remove = redisTemplate.opsForList().remove(key, count, value); return remove; } catch (Exception e) { e.printStackTrace(); return 0; } } }
新增完redis的配置后记得重启项目看看有没有问题
package com.study.test.common.api;//这是我存放ApiCode 的目录 public enum ApiCode { SUCCESS(200, "成功"), SYSTEM_ERROR(500, "操作失败"), NOT_FOUND(404,"未找到该资源"); private final int code; private final String msg; ApiCode(final int code, final String msg) { this.code = code; this.msg = msg; } public int getCode() { return code; } public String getMsg() { return msg; } }
package com.study.test.common.api;//这是我存放ApiResult的路径 import lombok.Data; import java.io.Serializable; import java.time.LocalDateTime; @Data public class ApiResult<T> implements Serializable { private static final long serialVersionUID = 1L; //状态码 private int code; //返回数据 private T data; //结果信息 private String message; //时间字符串 private String time; private ApiResult(){ } //定义成功的构造器 private ApiResult(T data){ this.code = ApiCode.SUCCESS.getCode(); this.message = ApiCode.SUCCESS.getMsg(); this.data = data; this.time = LocalDateTime.now().toString(); } private ApiResult(ApiCode apiCode){ this.code = apiCode.getCode(); this.message = apiCode.getMsg(); this.time = LocalDateTime.now().toString(); } private ApiResult(int code,String msg){ this.code = code; this.message = msg; this.time = LocalDateTime.now().toString(); } private ApiResult(ApiCode apiCode,T data){ this.code = apiCode.getCode(); this.message = apiCode.getMsg(); this.data = data; this.time = LocalDateTime.now().toString(); } /** * 成功的时候调用 * @param data * @return * @param <T> */ public static <T> ApiResult<T> success(T data){ return new ApiResult(data); } /** * 根据状态返回结果 * @param apiCode * @return * @param <T> */ public static <T> ApiResult<T> build(ApiCode apiCode){ return new ApiResult(apiCode); } /** * 根据code和msg返回结果 * @param code * @param msg * @return * @param <T> */ public static <T> ApiResult<T> build(int code,String msg){ return new ApiResult(code,msg); } /** * 根据状态和数据返回结果 * @param apiCode * @param data * @return * @param <T> */ public static <T> ApiResult<T> build(ApiCode apiCode,T data){ return new ApiResult(apiCode,data); } /** * 返回异常结果 * @param code * @param msg * @return * @param <T> */ public static <T> ApiResult<T> error(int code,String msg){ return new ApiResult(code,msg); } }
package com.study.test.common.exception;//这是我异常类存放的目录 import com.study.test.common.api.ApiCode; import lombok.Data; /** * 自定义异常类 */ @Data public class BusinessException extends RuntimeException{ private int code; private String msg; public BusinessException(ApiCode apiCode) { super(apiCode.getMsg()); this.code = apiCode.getCode(); this.msg = apiCode.getMsg(); } }
package com.study.test.common.exception;//这是我存放全局异常的目录 import com.study.test.common.api.ApiCode; import com.study.test.common.api.ApiResult; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; /** * 全局异常处理 */ @Slf4j @RestControllerAdvice public class GlobalExceptionHandler { //自定义异常 @ExceptionHandler(BusinessException.class) public ApiResult systemExceptionHandler(BusinessException e) { log.error("BusinessException全局异常:{}",e); return ApiResult.error(e.getCode(), e.getMsg()); } //系统异常 @ExceptionHandler(Exception.class) public ApiResult exceptionHandler(Exception e) { log.error("Exception全局异常:{}",e); return ApiResult.error(ApiCode.SYSTEM_ERROR.getCode(), e.getMessage()); } }
记得重启看看有没有问题
package com.study.test.common.utils;//这是我存放jwt工具类的目录 import com.auth0.jwt.JWT; import com.auth0.jwt.JWTCreator; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.interfaces.DecodedJWT; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import java.util.Calendar; import java.util.HashMap; import java.util.Map; @Component public class JwtUtils { private static String secretKey; private static Integer amount = 1800;//jwt的过期周期/秒 默认30分钟 @Value("${Jwt.secretKey}") public void secretKey(String secretKey) { JwtUtils.secretKey = secretKey; } /** * 创建token * @param payloadMap 存储的内容,自定义,一般是用户id * @return */ public static String generateToken(Map<String, String> payloadMap) { HashMap headers = new HashMap(); JWTCreator.Builder builder = JWT.create(); //定义jwt过期时间 Calendar instance = Calendar.getInstance(); instance.add(Calendar.SECOND, amount); //payload payloadMap.forEach((k, v) ->{ builder.withClaim(k, v); }); // 生成token String token = builder.withHeader(headers)//header //.withClaim("second",amount)//jwt的过期周期/秒,可以用于jwt快过期的时候自动刷新 .withExpiresAt(instance.getTime())//指定令牌的过期时间 .sign(Algorithm.HMAC256(secretKey));//签名 return token; } /** * 校验token是否合法 * @param token * @return */ public static DecodedJWT verifyToken(String token) { /* 如果有任何验证异常,此处都会抛出异常 SignatureVerificationException 签名不一致异常 TokenExpiredException 令牌过期异常 AlgorithmMismatchException 算法不匹配异常 InvalidClaimException 失效的payload异常 */ DecodedJWT decodedJWT = JWT.require(Algorithm.HMAC256(secretKey)).build().verify(token); return decodedJWT; } /** * 获取token信息 * @param token * @return */ public static DecodedJWT getTokenInfo(String token) { DecodedJWT decodedJWT = JWT.require(Algorithm.HMAC256(secretKey)).build().verify(token); return decodedJWT; } /** * 获取token信息方法 */ /*public static Map<String, Claim> getTokenInfo(String token) { return JWT.require(Algorithm.HMAC256(secretKey)).build().verify(token).getClaims(); }*/ }
package com.study.test.common.utils;//这是我存放web工具类的目录 import javax.servlet.http.HttpServletResponse; public class WebUtils { public static String rednerString(HttpServletResponse response, String content) { try{ response.setStatus(200); response.setContentType("application/json;charset=utf-8"); response.setCharacterEncoding("UTF-8"); response.getWriter().print(content); }catch (Exception e){ e.printStackTrace(); } return null; } }
package com.study.test.common.config;//这是我存放跨域的目录 import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.CorsFilter; //跨越请求配置类 @Configuration public class CorsConfig { private CorsConfiguration buildConfig() { CorsConfiguration corsConfiguration = new CorsConfiguration(); // 你需要跨域的地址 注意这里的 127.0.0.1 != localhost // * 表示对所有的地址都可以访问 corsConfiguration.addAllowedOrigin("*"); // 1 // 跨域的请求头 corsConfiguration.addAllowedHeader("*"); // 2 // 跨域的请求方法 corsConfiguration.addAllowedMethod("*"); // 3 //加上了这一句,大致意思是可以携带 cookie //最终的结果是可以 在跨域请求的时候获取同一个 session corsConfiguration.setAllowCredentials(true); return corsConfiguration; } @Bean public CorsFilter corsFilter() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); //配置 可以访问的地址 source.registerCorsConfiguration("/**", buildConfig()); return new CorsFilter(source); } }
参考下即可
package com.study.test.security.utils;//我存放AuthExceptionUtil的目录 import com.study.test.common.api.ApiResult; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.AuthorizationServiceException; import org.springframework.security.authentication.*; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.csrf.CsrfException; //认证异常工具类 public class AuthExceptionUtil { public static ApiResult getErrMsgByExceptionType(AuthenticationException e) { if (e instanceof LockedException) { return ApiResult.error(1100,"账户被锁定,请联系管理员!"); } else if (e instanceof CredentialsExpiredException) { return ApiResult.error(1105,"用户名或者密码输入错误!"); }else if (e instanceof InsufficientAuthenticationException) { return ApiResult.error(403,"请登录!"); } else if (e instanceof AccountExpiredException) { return ApiResult.error(1101, "账户过期,请联系管理员!"); } else if (e instanceof DisabledException) { return ApiResult.error(1102, ("账户被禁用,请联系管理员!")); } else if (e instanceof BadCredentialsException) { return ApiResult.error(1105, "用户名或者密码输入错误!"); }else if (e instanceof AuthenticationServiceException) { return ApiResult.error(1106, "认证失败,请重试!"); } return ApiResult.error(1200, e.getMessage()); } public static ApiResult getErrMsgByExceptionType(AccessDeniedException e) { if (e instanceof CsrfException) { return ApiResult.error(-1001, "非法访问跨域请求异常!"); } else if (e instanceof CsrfException) { return ApiResult.error(-1002,"非法访问跨域请求异常!"); } else if (e instanceof AuthorizationServiceException) { return ApiResult.error(1101, "认证服务异常请重试!"); }else if (e instanceof AccessDeniedException) { return ApiResult.error(4003, "权限不足不允许访问!"); } return ApiResult.error(1200, e.getMessage()); } }
package com.study.test.security.service;//我存放的目录 import com.study.test.common.utils.WebUtils; import com.study.test.security.utils.AuthExceptionUtil; import com.alibaba.fastjson.JSON; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; //自定义授权失败异常处理类 @Component public class AccessDeniedHandlerImpl implements AccessDeniedHandler { @Override public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException accessDeniedException) throws IOException, ServletException { System.out.println("AccessDeniedHandler:暂无权限"); WebUtils.rednerString(httpServletResponse, JSON.toJSONString(AuthExceptionUtil.getErrMsgByExceptionType(accessDeniedException))); } }
package com.study.test.security.service;//我存放的目录 import com.study.test.common.utils.WebUtils; import com.study.test.security.utils.AuthExceptionUtil; import com.alibaba.fastjson.JSON; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; //自定义认证失败异常处理类 @Component public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException authenticationException) throws IOException, ServletException { System.out.println("AuthenticationEntryPoint:用户未登录"); WebUtils.rednerString(httpServletResponse, JSON.toJSONString(AuthExceptionUtil.getErrMsgByExceptionType(authenticationException))); } }
重启下看看有没有问题,security的自定义异常就处理完了,接下来就是如何实现自定义登录了
我们自定义一个用户信息,要实现security的UserDetails
package com.gzgs.security.web.security.entity;//我存放的路径 import com.alibaba.fastjson.annotation.JSONField; import com.gzgs.security.web.entity.SysUser; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; import java.util.List; import java.util.stream.Collectors; @Data @NoArgsConstructor @AllArgsConstructor public class LogUser implements UserDetails { //用户信息 private SysUser user; //用户权限 private List<String> permissions; //存储SpringSecurity所需要的权限信息的集合 @JSONField(serialize = false) private List<SimpleGrantedAuthority> authorities; public LogUser(SysUser user,List<String> permissions){ this.user = user; this.permissions = permissions; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { // 将权限信息封装成 SimpleGrantedAuthority if (authorities != null) { return authorities; } //把permissions中字符串类型的权限信息转换成GrantedAuthority对象存入authorities中 authorities = this.permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList()); return authorities; } @Override public String getPassword() { return user.getPassword(); } @Override public String getUsername() { return user.getUserName(); } @Override public boolean isAccountNonExpired() { return user.getAccountNotExpired(); } @Override public boolean isAccountNonLocked() { return user.getAccountNotLocked(); } @Override public boolean isCredentialsNonExpired() { return user.getCredentialsNotExpired(); } @Override public boolean isEnabled() { return user.getEnabled(); } }
在loadUserByUsername方法写自己的登录逻辑,这里面涉及到查数据库
package com.study.test.security.service;//这是我的目录 import com.study.test.security.entity.LogUser; import com.study.test.web.entity.SysPermission; import com.study.test.web.entity.SysUser; import com.study.test.web.mapper.SysPermissionMapper; import com.study.test.web.mapper.SysUserMapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.List; @Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private SysPermissionMapper sysPermissionMapper; @Autowired private SysUserMapper userMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //需要构造出 org.springframework.security.core.userdetails.User 对象并返回 System.out.println("用户名:"+username); if (username == null || "".equals(username)) { throw new RuntimeException("用户不能为空"); } //根据用户名查询用户 SysUser user = userMapper.selectOne(new QueryWrapper<SysUser>().eq("account", username)); if (user == null) { throw new RuntimeException("用户不存在"); } List<String> permissionsList = new ArrayList<>(); if (user != null) { //获取该用户所拥有的权限 List<SysPermission> sysPermissions = sysPermissionMapper.selectPermissionList(user.getUserId()); // 声明用户授权 sysPermissions.forEach(sysPermission -> { permissionsList.add(sysPermission.getPermissionCode()); }); } //返回用户信息 return new LogUser(user,permissionsList); } //这是加密的算法,把加密后的密码update你用户表的数据库用户的密码上 public static void main(String[] args) { BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(); String encode = bCryptPasswordEncoder.encode("123456"); System.out.println(encode); } }
package com.study.test.security.filter; import com.study.test.common.exception.BusinessException; import com.study.test.common.utils.JwtUtils; import com.study.test.common.utils.RedisUtil; import com.study.test.security.entity.LogUser; import com.auth0.jwt.exceptions.TokenExpiredException; import com.auth0.jwt.interfaces.DecodedJWT; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Objects; @Component public class JwtAuthenticationFilter extends OncePerRequestFilter { @Autowired private RedisUtil redisUtil; //每次请求都会执行这个方法 @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws BusinessException, ServletException, IOException { // 获取Headers上的token,我命名为token String token = request.getHeader("token"); System.out.println("doFilterInternal:"+token); if (StringUtils.isEmpty(token)) { // token不存在 放行 并且直接return 返回 filterChain.doFilter(request, response); return; } // 解析token String userId = null; try { DecodedJWT tokenInfo = JwtUtils.verifyToken(token); //token过期时间 Date expiresAt = tokenInfo.getExpiresAt(); SimpleDateFormat ymdhms = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); System.out.println("token过期时间:"+ymdhms.format(expiresAt)); //其实这里后端可以做token是否快过期的处理,然后返回新的token给前端 //或者新写一个刷新tokena接口给前端,让前端自己刷新 userId = tokenInfo.getClaim("userId").asString(); } catch (Exception e) { if(e instanceof TokenExpiredException){ throw new RuntimeException("登录已过期!"); }else { throw new RuntimeException("token非法"); } } // 获取userid 从redis中获取用户信息 String redisKey = "login:" + userId; LogUser loginUser = (LogUser)redisUtil.get(redisKey); if (Objects.isNull(loginUser)) { throw new RuntimeException("用户未登录"); } //将用户信息存入到SecurityContextHolder UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authenticationToken); // 放行 filterChain.doFilter(request, response); } }
package com.study.test.security.config;//我的目录 import com.study.test.security.filter.JwtAuthenticationFilter; import com.study.test.security.service.AccessDeniedHandlerImpl; import com.study.test.security.service.AuthenticationEntryPointImpl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration //@EnableWebSecurity //因为我引入了spring-boot-starter-security,所以不用@EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) //开启权限注解,默认是关闭的 public class SecurityConfig extends WebSecurityConfigurerAdapter { //将authenticationManager注入容器中,再自定义登录接口中获取进行认证 @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Autowired private JwtAuthenticationFilter jwtAuthenticationFilter; @Autowired private AccessDeniedHandlerImpl accessDeniedHandler; @Autowired private AuthenticationEntryPointImpl authenticationEntryPoint; //注入加密方式--后面就会使用这种方式进行对密码的对比(明文与密码的对比是否匹配) // 而不使用默认的密码验证 @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } //配置放行的规则 @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() // 关闭csrf验证(防止跨站请求伪造攻击)由于我们的资源都会收到SpringSecurity的保护,所以想要跨域访问还要让SpringSecurity运行跨域访问 // 不通过session 获取SecurityContext(基于Token不需要session) .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() //开启权限拦截 .authorizeRequests() // 允许登录接口匿名访问 .antMatchers("/sysUser/login", "/sysUser/test","/test/**").anonymous() .antMatchers("/**.html","/js/**","/css/**","/img/**").permitAll()//放行静态资源 // 其他请求都需要认证 .anyRequest().authenticated(); //将jwtAuthenticationTokenFilter过滤器注入到UsernamePasswordAuthenticationFilter过滤器之前 http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); // 认证授权异常自定义处理 http.exceptionHandling() .authenticationEntryPoint(authenticationEntryPoint)//自定义认证失败异常处理类 .accessDeniedHandler(accessDeniedHandler);//自定义授权失败异常处理类 // 禁用缓存 http.headers().cacheControl(); // 跨域请求配置 http.cors(); } }
你再重新启动的时候,就看不到控制台输出密码了
我的项目结构
自定义登录的传参loginUserParam
package com.study.test.web.param;//我的目录
import lombok.Data;
@Data
public class LoginUserParam {
//用户名
private String userName;
//用户密码
private String password;
}
package com.study.test.web.service;//我的目录
import com.study.test.common.api.ApiResult;
import com.study.test.web.param.LoginUserParam;
public interface LogService {
ApiResult login(LoginUserParam param);
ApiResult logOut();
}
package com.study.test.web.service.impl;//我的目录 import com.study.test.common.api.ApiResult; import com.study.test.common.utils.JwtUtils; import com.study.test.common.utils.RedisUtil; import com.study.test.security.entity.LogUser; import com.study.test.web.param.LoginUserParam; import com.study.test.web.service.LogService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; import java.util.HashMap; import java.util.Map; import java.util.Objects; @Service public class LogServiceImpl implements LogService { @Autowired private RedisUtil redisUtil; @Autowired private AuthenticationManager authenticationManager; @Override public ApiResult login(LoginUserParam param) { // 1 获取AuthenticationManager 对象 然后调用 authenticate() 方法 // UsernamePasswordAuthenticationToken 实现了Authentication 接口 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(param.getUserName(), param.getPassword()); Authentication authenticate = authenticationManager.authenticate(authenticationToken); //2 认证没通过 提示认证失败 if (Objects.isNull(authenticate)) { throw new RuntimeException("认证失败用户信息不存在"); } //认证通过 使用userid 生成jwt token令牌 LogUser loginUser = (LogUser) authenticate.getPrincipal(); String userId = loginUser.getUser().getUserId().toString(); Map<String, String> payloadMap = new HashMap<>(); payloadMap.put("userId", userId); payloadMap.put("userName", loginUser.getUser().getUserName()); payloadMap.put("token", JwtUtils.generateToken(payloadMap)); boolean resultRedis = redisUtil.set("login:" + userId, loginUser); if(!resultRedis){ throw new RuntimeException("redis连接不上,登录失败"); } return ApiResult.success(payloadMap); } @Override public ApiResult logOut() { // 1 获取 SecurityContextHolder 中的用户id UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); LogUser loginUser = (LogUser)authentication.getPrincipal(); //2 删除redis 中的缓存信 String key = "login:"+loginUser.getUser().getUserId().toString(); redisUtil.del(key); return ApiResult.success("退出成功!"); } }
package com.study.test.web.controller; import com.study.test.common.api.ApiResult; import com.study.test.web.param.LoginUserParam; import com.study.test.web.service.LogService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/sysUser") public class SysUserController { @Autowired private LogService logService; /** * 自定义登录 * @param param 登录传参 * @return */ @PostMapping("/login") public ApiResult login(@RequestBody LoginUserParam param) { return logService.login(param); } /** * 自定义登出 * @return */ @PostMapping("/logOut") public ApiResult logOut() { return logService.logOut(); } }
我自定义登录的请求为/sysUser/login 所以你得看看有没有把这个请求放行了,不然会提示无权限访问
登入
登出失败
登出成功
配置自定义权限校验方法
package com.study.test.security.handler;//我的目录 import com.study.test.security.entity.LogUser; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import java.util.List; //自定义security权限校验方法 @Component("syex") public class SecurityPermissionsExpression { public boolean hasAuthority(String authority){ //获取当前用户的权限 Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); LogUser loginUser = (LogUser) authentication.getPrincipal(); List<String> permissions = loginUser.getPermissions(); //判断用户权限集合中是否存在authority return permissions.contains(authority); } }
到这里,security的配置基本结束了,下面是我的目录结构
我的权限【sys:queryUser】
在控制层配置权限
package com.study.test.web.controller; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/testPreAuthorize") public class TestPreAuthorizeController { @PostMapping("/hello") // 只有sys:queryUser 权限才能访问 //@PreAuthorize("hasAuthority('sys:queryUser')") //这是没有自定义权限校验方法的默认写法 @PreAuthorize("@syex.hasAuthority('sys:queryUser')") public String hello(){ return "hello"; } @PostMapping("/hello2") // 只有sys:queryUser2 权限才能访问 @PreAuthorize("@syex.hasAuthority('sys:queryUser2')") public String hello2(){ return "hello2"; } }
Postman测试权限
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。