项目开发前的准备
1. 编写mybatisPlus配置类
本项目分页使用mybatisPlus的分页功能。(pageHelper也挺好用,想用这个可以自己网上查询用法)
@Configuration
@MapperScan("com.zw.web.*.mapper")
public class MyBatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
2.编写MybatisPlus元数据处理器
自动写入创建时间,创建人,更新时间,更新者。便于记录数据操作
@Repository
@Slf4j
public class MyBatisPlusHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
log.info("start insert fill ....");
this.setFieldValByName("createTime", LocalDateTime.now(), metaObject);
this.setFieldValByName("updateTime",LocalDateTime.now(),metaObject);
}
@Override
public void updateFill(MetaObject metaObject) {
log.info("start update fill ...");
this.setFieldValByName("updateTime",LocalDateTime.now(),metaObject);
}
}
3.配置redis模板类,配置连接。
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory){
// 创建RedisTemplate对象
RedisTemplate<String, Object> template = new RedisTemplate<>();
// 设置连接工厂
template.setConnectionFactory(connectionFactory);
// 创建JSON序列化工具
GenericJackson2JsonRedisSerializer jsonRedisSerializer =
new GenericJackson2JsonRedisSerializer();
// 设置Key的序列化
template.setKeySerializer(RedisSerializer.string());
template.setHashKeySerializer(RedisSerializer.string());
// 设置Value的序列化
template.setValueSerializer(jsonRedisSerializer);
template.setHashValueSerializer(jsonRedisSerializer);
// 返回
return template;
}
}
spring:
redis:
host: 192.168.184.129
port: 6379
password: 123456
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
max-wait: 100ms
4.整合Swagger2(较好用),配置文件兼容问题
@Configuration
@EnableSwagger2
public class Swagger2Config {
@Bean//设置接口框架的基本信息
public Docket createRestApi(){
return new Docket(DocumentationType.SWAGGER_2)
//接口描述信息
.apiInfo(apiInfo())
.select()
//扫描哪些包创建接口文档
.apis(RequestHandlerSelectors.basePackage("top.psjj.wy"))
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo(){
return new ApiInfoBuilder()
.title("接口文档")
.description("物业系统接口文档")
.contact(new Contact("胖叔讲Java","http://127.0.0.1:8888/doc.html","xxx@xx.com"))
.version("3.1")
.build();
}
}
spring:
mvc:
pathmatch:
matching-strategy: ant_path_matcher
5.配置跨域工具类
相比在类上写注解来跨域(@Crossorigin),配置类跨域可以一劳永逸。
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter(){
CorsConfiguration config = new CorsConfiguration();
//接受任意域名的请求
config.addAllowedOrigin("*");
//不支持提交COOKIE数据
config.setAllowCredentials(false);
//绑定请求头信息,使用通配符*接受任意字段
config.addAllowedHeader("*");
//支持任意提交方法
config.addAllowedMethod("*");
UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource();
configSource.registerCorsConfiguration("/**",config);
return new CorsFilter(configSource);
}
}
6.返回结果集封装
可以统一响应结果,前后端交互起来更简便。
@Data
public class Result<T> {
//状态码
private Integer code;
//响应消息
private String msg;
//响应数据
private T data;
//响应成功
private static final int OK_CODE = 200;
//响应成功消息
private static final String OK_MSG = "SUCCESS";
//响应失败
private static final int SERVER_ERROR = 500;
private static final String SERVER_ERROR_MSG = "服务器异常";
//未认证
private static final int UNAUTHORIZED = 401;
private static final String UNAUTHORIZED_MSG = "未认证";
//权限不足
private static final int PERMISSION_DENIED = 403;
private static final String PERMISSION_DENIED_MSG = "权限不足";
public Result(Integer code,String msg,T data){
this.code = code;
this.msg = msg;
this.data= data;
}
public Result(Integer code,String msg){
this.code = code;
this.msg = msg;
}
//响应成功
public Result(){
this(OK_CODE,OK_MSG);
}
public static Result success(){
return new Result();
}
public static <T> Result<T> success(T data){
Result<T> result = new Result<>(OK_CODE,OK_MSG,data);
return result;
}
public static Result error(Integer code,String msg){
return new Result(code,msg);
}
public static Result error(){
return new Result(SERVER_ERROR,SERVER_ERROR_MSG);
}
public static Result unauthorized(){
return new Result(UNAUTHORIZED,UNAUTHORIZED_MSG);
}
public static Result permissionDenied(){
return new Result(PERMISSION_DENIED,PERMISSION_DENIED_MSG);
}
}
7.编写jwt工具类
用于生成和获取token令牌及令牌中的信息。
@Data
@ConfigurationProperties(prefix = "jwt")
@Component
public class JwtUtils {
//密钥
private String secret;
// 过期时间 毫秒
private Long expiration;
/**
* 从数据声明生成令牌
*
* @param claims 数据声明
* @return 令牌
*/
private String generateToken(Map<String, Object> claims) {
Date expirationDate = new Date(System.currentTimeMillis() + expiration);
return Jwts.builder().setClaims(claims).setExpiration(expirationDate).signWith(SignatureAlgorithm.HS512, secret).compact();
}
/**
* 生成令牌
*
* @param userId 用户id
* @return 令牌
*/
public String generateToken(Integer userId,String username,Integer userType) {
Map<String, Object> claims = new HashMap<>(4);
//设置token分类
claims.put(Claims.SUBJECT,"WUYE_USER");
//设置私有信息,用户信息
claims.put("userId",userId);
claims.put("username",username);
claims.put("userType",userType);
//设置签发时间为当前时间
claims.put(Claims.ISSUED_AT, new Date());
return generateToken(claims);
}
/**
* 从令牌中获取数据声明
*
* @param token 令牌
* @return 数据声明
*/
public Claims getClaimsFromToken(String token) {
Claims claims;
try {
claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
} catch (Exception e) {
claims = null;
}
return claims;
}
/**
* 从令牌中获取用户id
*
* @param token 令牌
* @return 用户id
*/
public Integer getUserIdFromToken(String token) {
Integer userId;
try {
Claims claims = getClaimsFromToken(token);
userId = (Integer) claims.get("userId");
} catch (Exception e) {
userId = null;
}
return userId;
}
/**
* 从令牌中获取用户名
*
* @param token 令牌
* @return 用户名
*/
public String getUsernameFromToken(String token) {
String username;
try {
Claims claims = getClaimsFromToken(token);
username = (String)claims.get("username");
} catch (Exception e) {
username = null;
}
return username;
}
/**
* 从令牌中获取用户类型
*
* @param token 令牌
* @return 用户类型
*/
public Integer getUserTypeFromToken(String token) {
Integer userType;
try {
Claims claims = getClaimsFromToken(token);
userType = (Integer)claims.get("userType");
} catch (Exception e) {
userType = null;
}
return userType;
}
/**
* 刷新令牌
*
* @param token 原令牌
* @return 新令牌
*/
public String refreshToken(String token) {
String refreshedToken;
try {
Claims claims = getClaimsFromToken(token);
claims.put(Claims.ISSUED_AT, new Date());
refreshedToken = generateToken(claims);
} catch (Exception e) {
refreshedToken = null;
}
return refreshedToken;
}
}
#jwt配置
jwt:
secret: pangshujiangjava
#30分钟过期
expiration: 1800000
8.配置全局异常处理类
@RestControllerAdvice("top.psjj.wy")
@Slf4j
public class GlobalException {
@ExceptionHandler(Exception.class)
public Object handlerException(Exception e){
log.error("系统异常"+e.getMessage());
return Result.error(500,e.getMessage());
}
}
9.项目提交git
记住下面几个指令即可:git init git add . git commit -m'描述' git pull origin git push origin。
登录模块:基于SpringSecurity
编写SecurityConfig配置类,其中包括注入密码加密器、认证管理器、过滤器链
//注入过滤链
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
//关闭csrf()攻击防护
http.csrf().disable();
//允许跨域
http.cors();
//关闭iframe窗口防护
http.headers().frameOptions().disable();
//关闭session会话
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
//配置认证过滤器
http.addFilterAfter(jwtAuthenticationTokenFilter,UsernamePasswordAuthenticationFilter.class);
//配置所有请求必须认证
http.authorizeRequests().anyRequest().authenticated();
//配置认证失败处理
http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
return http.build();
}
DaoAuthenticationProvider
// 注入DaoAuthenticationProvider
@Bean
public DaoAuthenticationProvider authenticationProvider(){
DaoAuthenticationProvider auth = new DaoAuthenticationProvider();
auth.setUserDetailsService(userDetailsService);
auth.setPasswordEncoder(passwordEncoder());
return auth;
}
配置忽略路径
//配置忽略路径
@Bean
public WebSecurityCustomizer securityCustomizer() throws Exception{
return (web) -> {
web.ignoring().antMatchers("/api/captcha",
"/api/login",
"/doc.html",
"/webjars/**",
"/swagger-resources/**",
"/v2/api-docs/**");
};
}
后端具体实现
验证码实现
1.使用CaptchaUtil工具类创建一个验证码图片
2.获取验证码内容(code)和验证码ID,以前缀+ID为key,码为值存入到redis数据库中
3.向前端响应数据,包括验证码ID和getImageBase64Data()方法生成的带前缀的验证码图片压缩的字符串。
public Result captcha(){
//1.获取验证码
LineCaptcha captcha = CaptchaUtil.createLineCaptcha(200, 40, 4, 20);
//获取验证码压缩图片
String imageBase64Data = captcha.getImageBase64Data();
//获取验证码内容
String captchaCode = captcha.getCode();
//获取验证码id
String captchaId = UUID.randomUUID().toString();
//2.将验证码信息存到redis中 key 为wy:login:captcha:+id redis中会以冒号为分割创建文件夹
redisTemplate.opsForValue().set(RedisConstant.CAPTCHA_PRE+captchaId,
captchaCode,RedisConstant.CAPTCHA_EXPIRE_TIME, TimeUnit.SECONDS);
//3.响应数据
HashMap<String,Object> map = new HashMap<>();
map.put("captchaId",captchaId);
map.put("imageBase64",imageBase64Data);
return Result.success(map);
}
jwt认证过滤
1.继承OncePerRequestFilter类,重写doFilterInternal()方法
2. 从请求头中获取token,如果没有token(未登录)放行,有就继续
3.解析token(包括创建token需要的信息)
4.刷新token和redis中存储的用户的有效期
5.将用户信息存入securityContextHolder中,使得过滤链上每个环节都能通过SecurityContextHolder拿到用户信息。放行。
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private RedisTemplate<String,Object> redisTemplate;
@Autowired
private JwtUtils jwtUtils;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//1.获取token
String token = request.getHeader("authorization");
//System.out.println(token);
if (!StringUtils.hasText(token)){
//放行
filterChain.doFilter(request,response);
return;
}
//2.解析token
Integer userId;
Integer userType;
try {
userId = jwtUtils.getUserIdFromToken(token);
userType = jwtUtils.getUserTypeFromToken(token);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("token非法");
}
//3.刷新token和redis的有效期
String refreshToken = jwtUtils.refreshToken(token);
response.setHeader("Access-Control-Expose-Headers","Authorization");
response.addHeader("Authorization",refreshToken);
if (SystemConstant.USER_TYPE_WUZHU==userType){
SysUser sysUser = (SysUser) redisTemplate.opsForValue().get(RedisConstant.LOGIN_SYSTEM_USER_PRE + userId);
if (Objects.isNull(sysUser)){
throw new RuntimeException("用户未登录");
}
//刷新redis有效期
redisTemplate.expire(RedisConstant.LOGIN_SYSTEM_USER_PRE + userId, RedisConstant.LOGIN_SYSTEM_USER_EXPIRE_TIME, TimeUnit.MINUTES);
//3.将用户信息存入securityContextHolder中
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(sysUser, null, null);
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
filterChain.doFilter(request,response);
}else {
LiveUser liveUser = (LiveUser) redisTemplate.opsForValue().get(RedisConstant.LOGIN_LIVE_USER_PRE + userId);
if (Objects.isNull(liveUser)){
throw new RuntimeException("用户未登录");
}
redisTemplate.expire(RedisConstant.LOGIN_LIVE_USER_PRE+userId,RedisConstant.LOGIN_LIVE_USER_EXPIRE_TIME,TimeUnit.MINUTES);
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(liveUser, null, null);
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
filterChain.doFilter(request,response);
}
}
}
登录接口
返回result带有token令牌的map集合
实现UserDetailsService
基于数据库查询用户名对应的用户信息
登录业务层
1.校验验证码
从redis中取出验证码和前端输入的验证码将进行比较,错误抛出异常
private void validCaptcha(String captchaId, String captchaCode) {
//1.从redis中获取验证码
String captchaCode2 = (String) redisTemplate.opsForValue().get(RedisConstant.CAPTCHA_PRE + captchaId);
if (!captchaCode.equalsIgnoreCase(captchaCode2)){
throw new RuntimeException("验证码有误");
}
}
2.校验用户名密码返回认证信息
①将用户名密码封装成usernamePasswordAuthenticationToken
②通过authenticationManager调用认证方法,返回认证对象,认证对象为空抛出异常,反之返回认证对象
private Authentication validUsernameAndPassword(String username, Integer userType, String password) {
username = username+":"+userType;
//将用户名密码封装成usernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, password);
//进行认证
Authentication authentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken);
if(Objects.isNull(authentication)){
throw new RuntimeException("用户名或密码错误");
}
return authentication;
}
3.将用户信息存入redis,并响应token数据
1.用户信息在认证对象的主体中
2.创建token返回result带有token令牌的map集合
private Result responseToken(Integer userType, Authentication authentication) {
int userId;
String username;
//用户类型为物主
if (userType== SystemConstant.USER_TYPE_WUZHU){
SysUser sysUser = (SysUser) authentication.getPrincipal();
userId = sysUser.getUserId();
username = sysUser.getUsername();
//将物主信息存到redis中
redisTemplate.opsForValue().set(RedisConstant.LOGIN_SYSTEM_USER_PRE + userId, sysUser, RedisConstant.LOGIN_SYSTEM_USER_EXPIRE_TIME, TimeUnit.MINUTES);
}else {
LiveUser liveUser = (LiveUser) authentication.getPrincipal();
userId = liveUser.getUserId();
username = liveUser.getUsername();
//将业主信息存入redis
redisTemplate.opsForValue().set(RedisConstant.LOGIN_LIVE_USER_PRE + userId, liveUser, RedisConstant.LOGIN_LIVE_USER_EXPIRE_TIME, TimeUnit.MINUTES);
}
//创建token
String token = jwtUtils.generateToken(userId, username, userType);
HashMap<String, String> map = new HashMap<>();
map.put("token",token);
return Result.success(map);
}
前端具体实现
1.验证码获取,将返回结果绑定到img标签的src属性中,带有前缀会自动解压
2.login() 方法,将响应结果中的token存储到session storage中
3.配置http.js
①请求之前的拦截器,从session storage中获取token添加到请求头中(key为后端jwt过滤器中获取token的key)
//请求发送之前的拦截器
axios.interceptors.request.use(
config => {
let token = sessionStorage.getItem("authorization")
//如果token存在,把token添加到请求的头部
if (token) {
config.headers['authorization'] = token
}
return config
},
error => {
console.log(error)
return Promise.reject(error)
}
)
②请求返回之后的处理,从响应头中获取token并更新session storage中的token(前端刷新token)
//请求返回之后的处理
axios.interceptors.response.use(
response => {
if(response.headers.authorization){
sessionStorage.setItem("authorization",response.headers.authorization)
}
const res = response.data
if (res.code !== 200) {
Message({
message: res.msg || '服务器出错',
type: 'error',
duration: 5 * 1000
})
return Promise.reject(new Error(res.msg || '服务器出错'))
} else {
return res
}
},
error => {
console.log('err' + error)
Message({
message: error.msg || '服务器出错!',
type: 'error',
duration: 5 * 1000
})
return Promise.reject(error)
}
)
用户管理模块
角色管理和菜单管理模块
权限管理模块
房屋管理模块
物业系统管理模块
动态菜单
密码加密传输
3.1 节 加密的几种方式
在Java开发的过程中,很多场景下都需要加密解密,比如对敏感数据的加密,对配置文件信息的加密,通信数据的加密等等。
加密分为三类:
-
摘要加密(digest)
-
对称加密(symmetric)
-
非对称加密(asymmetric)
3.1.1 摘要加密(digest)
说明:数字摘要是将任意长度的消息变成固定长度的短消息,它类似于一个自变量是消息的函数,也就是Hash函数。数字摘要就是采用单向Hash函数将需要加密的明文“摘要”成一串固定长度(128位)的密文这一串密文又称为数字指纹,它有固定的长度,而且不同的明文摘要成密文,其结果总是不同的,而同样的明文其摘要必定一致。 常见的有:
-
MD5
-
SHA
-
16进制编码
-
Base64编码
3.1.2 对称加密算法(symmetric)
对称加密算法是应用较早的加密算法,技术成熟。在对称加密算法中,数据发信方将明文(原始数据)和加密密钥(mi yao)一起经过特殊加密算法处理后,使其变成复杂的加密密文发送出去。收信方收到密文后,若想解读原文,则需要使用加密用过的密钥及相同算法的逆算法对密文进行解密,才能使其恢复成可读明文。在对称加密算法中,使用的密钥只有一个,发收信双方都使用这个密钥对数据进行加密和解密,这就要求解密方事先必须知道加密密钥。
常见的有:
-
DES
-
AES(新)
4.1.3 非对称加密 (asymmetric)
非对称加密算法:又称为公开密钥加密算法,需要两个密钥,一个为公开密钥(PublicKey)即公钥,一个为私有密钥(PrivateKey)即私钥。两者需要配对使用。用其中一者加密,则必须用另一者解密。
常见的有:
-
RSA 算法
-
数字签名
3.2 节 常见加密工具类使用
Hutool-all包含Hutool-crypto模块,hutool-crypto中包含创建的算法工具类,具体如下: