一、登录功能开发
前期准备:
编写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.验证码实现:
(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);
}
2.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);
}
}
}
3.登录接口
返回result带有token令牌的map集合
4.实现UserDetailsService
基于数据库查询用户名对应的用户信息
5.登录业务层
(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数据
①用户信息在认证对象的主体中
②创建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(新)
3.1.3 非对称加密 (asymmetric)
非对称加密算法:又称为公开密钥加密算法,需要两个密钥,一个为公开密钥(PublicKey)即公钥,一个为私有密钥(PrivateKey)即私钥。两者需要配对使用。用其中一者加密,则必须用另一者解密。
常见的有:
-
RSA 算法
-
数字签名
3.2 节 常见加密工具类使用
Hutool-all包含Hutool-crypto模块,hutool-crypto中包含创建的算法工具类,具体如下: