文章目录
- 登陆校验流程
- 依赖
- yaml
- 实现
- 建表、工具类、实体类
- 加密器、AuthenticationManager
- 登录逻辑
- 登录过滤器、配置过滤器
- 登出
登陆校验流程
认证
登录:
①自定义登录接口
调用ProviderManager的方法进行认证
如果认证通过生成token,根据userId把用户信息存入redis中,返回token给前端
②自定义UserDetailsService
在这个实现类中去查询数据库,封装为UserDetails对象返回
校验:
①定义Jwt认证过滤器
获取token
解析token获取其中的userid
从redis中获取用户信息
存入SecurityContextHolder
依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--redis依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--fastjson依赖-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.33</version>
</dependency>
<!--jwt依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- security-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
yaml
spring:
datasource:
url: jdbc:mysql://192.168.111.101:3306/security?characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
实现
建表、工具类、实体类
建表语句
CREATE TABLE `user` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`user_name` VARCHAR(64) NOT NULL DEFAULT 'NULL' COMMENT '用户名',
`nick_name` VARCHAR(64) NOT NULL DEFAULT 'NULL' COMMENT '昵称',
`password` VARCHAR(64) NOT NULL DEFAULT 'NULL' COMMENT '密码',
`status` CHAR(1) DEFAULT '0' COMMENT '账号状态(0正常 1停用)',
`email` VARCHAR(64) DEFAULT NULL COMMENT '邮箱',
`phonenumber` VARCHAR(32) DEFAULT NULL COMMENT '手机号',
`sex` CHAR(1) DEFAULT NULL COMMENT '用户性别(0男,1女,2未知)',
`avatar` VARCHAR(128) DEFAULT NULL COMMENT '头像',
`user_type` CHAR(1) NOT NULL DEFAULT '1' COMMENT '用户类型(0管理员,1普通用户)',
`create_by` BIGINT(20) DEFAULT NULL COMMENT '创建人的用户id',
`create_time` DATETIME DEFAULT NULL COMMENT '创建时间',
`update_by` BIGINT(20) DEFAULT NULL COMMENT '更新人',
`update_time` DATETIME DEFAULT NULL COMMENT '更新时间',
`del_flag` INT(11) DEFAULT '0' COMMENT '删除标志(0代表未删除,1代表已删除)',
PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='用户表'
JWT工具类
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.UUID;
/**
* JWT工具类
*/
public class JwtUtil {
//有效期为
public static final Long JWT_TTL = 60 * 60 *1000L;// 60 * 60 *1000 一个小时
//设置秘钥明文
public static final String JWT_KEY = "xd52s8w";
public static String getUUID(){
String token = UUID.randomUUID().toString().replaceAll("-", "");
return token;
}
/**
* 生成jtw
* @param subject token中要存放的数据(json格式)
* @return
*/
public static String createJWT(String subject) {
JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间
return builder.compact();
}
/**
* 生成jtw
* @param subject token中要存放的数据(json格式)
* @param ttlMillis token超时时间
* @return
*/
public static String createJWT(String subject, Long ttlMillis) {
JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间
return builder.compact();
}
private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
SecretKey secretKey = generalKey();
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
if(ttlMillis==null){
ttlMillis=JwtUtil.JWT_TTL;
}
long expMillis = nowMillis + ttlMillis;
Date expDate = new Date(expMillis);
return Jwts.builder()
.setId(uuid) //唯一的ID
.setSubject(subject) // 主题 可以是JSON数据
.setIssuer("sg") // 签发者
.setIssuedAt(now) // 签发时间
.signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥
.setExpiration(expDate);
}
/**
* 创建token
* @param id
* @param subject
* @param ttlMillis
* @return
*/
public static String createJWT(String id, String subject, Long ttlMillis) {
JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间
return builder.compact();
}
public static void main(String[] args) throws Exception {
String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJjYWM2ZDVhZi1mNjVlLTQ0MDAtYjcxMi0zYWEwOGIyOTIwYjQiLCJzdWIiOiJzZyIsImlzcyI6InNnIiwiaWF0IjoxNjM4MTA2NzEyLCJleHAiOjE2MzgxMTAzMTJ9.JVsSbkP94wuczb4QryQbAke3ysBDIL5ou8fWsbt_ebg";
Claims claims = parseJWT(token);
System.out.println(claims);
}
/**
* 生成加密后的秘钥 secretKey
* @return
*/
public static SecretKey generalKey() {
byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return key;
}
/**
* 解析
*
* @param jwt
* @return
* @throws Exception
*/
public static Claims parseJWT(String jwt) throws Exception {
SecretKey secretKey = generalKey();
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getBody();
}
}
实体类
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.Date;
/**
* 用户表(User)实体类
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
private Long id;
/**
* 用户名
*/
private String userName;
/**
* 昵称
*/
private String nickName;
/**
* 密码
*/
private String password;
/**
* 账号状态(0正常 1停用)
*/
private String status;
/**
* 邮箱
*/
private String email;
/**
* 手机号
*/
private String phonenumber;
/**
* 用户性别(0男,1女,2未知)
*/
private String sex;
/**
* 头像
*/
private String avatar;
/**
* 用户类型(0管理员,1普通用户)
*/
private String userType;
/**
* 创建人的用户id
*/
private Long createBy;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新人
*/
private Long updateBy;
/**
* 更新时间
*/
private Date updateTime;
/**
* 删除标志(0代表未删除,1代表已删除)
*/
private Integer delFlag;
}
UserDetails: loadUserByUsername 方法返回的数据
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUser implements UserDetails {
private User user;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return user.getPassword ();
}
@Override
public String getUsername() {
return user.getUserName ();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
UserDetailsService ,通过loadUserByUsername获取用户,封装成UserDetails 对象返回
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.example.security.mapper.UserMapper;
import com.example.security.vo.LoginUser;
import com.example.security.vo.User;
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.stereotype.Service;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 查询用户信息
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<> ();
queryWrapper.eq (User::getUserName, username);
User user = userMapper.selectOne (queryWrapper);
if (user == null) {
// 用户不存在
throw new RuntimeException ("用户不存在");
}
//TODO 根据用户查询权限信息 添加到LoginUser中
return new LoginUser (user);
}
}
加密器、AuthenticationManager
密码加密存储、登陆接口
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 密码加密存储
*
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder ();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//关闭csrf
.csrf ().disable ()
//不通过Session获取SecurityContext
.sessionManagement ().sessionCreationPolicy (SessionCreationPolicy.STATELESS)
.and ()
.authorizeRequests ()
// 对于登录接口 允许匿名访问
.antMatchers ("/user/login").anonymous ()
// 除上面外的所有请求 全部需要鉴权认证
.anyRequest ().authenticated ();
}
/**
* 登陆接口
* 通过AuthenticationManager的authenticate方法来进行用户认证
* 所以需要在SecurityConfig中配置把AuthenticationManager注入容器
*
* @return
* @throws Exception
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean ();
}
}
登录逻辑
LoginServiceImpl 登录逻辑
import com.alibaba.fastjson.JSONObject;
import com.example.security.config.JwtUtil;
import com.example.security.vo.LoginUser;
import com.example.security.vo.ResponseResult;
import com.example.security.vo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Objects;
@Service
public class LoginServiceImpl implements LoginServcie {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private StringRedisTemplate redisTemplate;
@Override
public ResponseResult login(User user) {
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken (user.getUserName (), user.getPassword ());
Authentication authenticate = authenticationManager.authenticate (authenticationToken);
if (Objects.isNull (authenticate)) {
throw new RuntimeException ("用户名或密码错误");
}
// 使用userId生成token
LoginUser loginUser = (LoginUser) authenticate.getPrincipal ();
String userId = loginUser.getUser ().getId ().toString ();
String token = JwtUtil.createJWT (userId);
// authenticate存入redis
redisTemplate.opsForValue ().set ("login:" + userId, JSONObject.toJSONString (loginUser));
// 把token响应给前端
HashMap<String, String> map = new HashMap<> ();
map.put ("token", token);
return new ResponseResult (200, "登陆成功", map);
}
}
登录过滤器、配置过滤器
对需要登录的接口进行过滤,从redis中查询用户信息,未登录就 不放行
import com.alibaba.fastjson.JSONObject;
import com.example.security.vo.LoginUser;
import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
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.util.Objects;
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 对于需要登录的接口进行拦截
* 看看用户信息是否存在
*
* @param request
* @param response
* @param filterChain
* @throws ServletException
* @throws IOException
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 获取 token
String token = request.getHeader ("token");
if (!StringUtils.hasText (token)) {
// 不携带token,放行
filterChain.doFilter (request, response);
return;
}
//解析token
String userId;
try {
Claims claims = JwtUtil.parseJWT (token);
userId = claims.getSubject ();
} catch (Exception e) {
e.printStackTrace ();
throw new RuntimeException ("token非法");
}
//从redis中获取用户信息
String redisKey = "login:" + userId;
LoginUser loginUser = JSONObject.parseObject (redisTemplate.opsForValue ().get (redisKey), LoginUser.class);
if (Objects.isNull (loginUser)) {
// 没有token,就是未登录
throw new RuntimeException ("用户未登录");
}
//存入SecurityContextHolder
//TODO 获取权限信息封装到Authentication中
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken (loginUser, null, null);
SecurityContextHolder.getContext ().setAuthentication (authenticationToken);
// 放行
filterChain.doFilter (request, response);
}
}
配置过滤器,使其生效
@Autowired
private JwtAuthenticationTokenFilter filter;
@Override
protected void configure(HttpSecurity http) throws Exception {
// 把token校验过滤器 添加到过滤器链中
http.addFilterBefore (filter, UsernamePasswordAuthenticationFilter.class);
http
//关闭csrf
.csrf ().disable ()
//不通过Session获取SecurityContext
.sessionManagement ().sessionCreationPolicy (SessionCreationPolicy.STATELESS)
.and ()
.authorizeRequests ()
// 对于登录接口 允许匿名访问
.antMatchers ("/user/login").anonymous ()
// 除上面外的所有请求 全部需要鉴权认证
.anyRequest ().authenticated ();
}
登出
将用户信息从Redis中删除
登出
public ResponseResult logout() {
Authentication authentication = SecurityContextHolder.getContext ().getAuthentication ();
LoginUser loginUser = (LoginUser) authentication.getPrincipal ();
Long userid = loginUser.getUser ().getId ();
redisTemplate.delete ("login:" + userid);
return new ResponseResult (200, "退出成功");
}