下面我会根据该流程图去自定义接口:
我们需要做的任务有:
登陆:1、通过ProviderManager的方法进行认证,生成jwt;2、把用户信息存入redis;3、自定义UserDetailsService实现到数据库查询数据的方法。
校验:自定义一个jwt认证过滤器,其实现功能:获取token;解析token;从redis获取信息;存入SecurityContextHolder。
登陆:
图中的 5.1步骤是到内存中查询用户信息,而我们需要的是到数据库中查询。而图中查询用户信息是调用loadUserbyUsername方法实现的。
所以我们需要实现UserDetailsService接口并重写该方法:(下面案例中我用的mybatis plus实现的查询数据库)
java
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
//loadUserByUsername方法即为流程图中查询用户信息的方法。
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//查询用户信息
LambdaQueryWrapper<User> queryWrapper= new LambdaQueryWrapper<>();
queryWrapper.eq(User::getUserName,username);
User user = userMapper.selectOne(queryWrapper);
//如果没有查询到用户
if(Objects.isNull(user)){
throw new RuntimeException("用户名或密码错误");
}
//封装为UserDetails类型返回
return new LoginUser(user);
}
}
我们先写好登陆功能的controller层代码:
java
@RestController
public class LoginController {
@Autowired
private LoginService loginService;
@PostMapping("/user/login")
public ResponseResult login(@RequestBody User user){
//登陆
return loginService.login(user);
}
我们需要让springSecurity对该登陆接口放行,不需要登陆就能访问。在登陆service层接口中需要通过AuthenticationManager的authenticate方法进行用户认证,我们先在SecurityConfig中把AuthenticationManager注入容器。
java
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
@Bean //(name = "") //获取AuthenticationManager的bean,因为现在只有这一个AuthenticationManager,所以不写也没事。
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
//放开接口
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//关闭csrf,csrf为跨域策略,不支持post
.csrf().disable()
//不通过session获取SecurityContext 前后端分离时session不可用
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
//对于登陆接口,允许匿名访问,登陆之后不允许访问,只允许匿名的用户,可以防止重复登陆。
.antMatchers("/user/login").anonymous() //permitAll() 登录能访问,不登录也能访问,一般用于静态资源js等
//除了上面外,所有请求需要鉴权认证
.anyRequest().authenticated();//authenticated():任意用户,认证后都可访问。
}
}
然后我们去修改登陆接口的service层实现类代理:
java
@Service
public class LoginServiceImpl implements LoginService {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private RedisCache redisCache;
//登陆
@Override
public ResponseResult login(User user) {
//获取AuthenticationManager的authenticate方法进行认证。
//通过SecurityConfig获取AuthenticationManager
//创建Authentication,第一个参数为认证主体,没有的话传用户名,第二个参数传密码
UsernamePasswordAuthenticationToken authenticationToken
=new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());
//需要Authentication参数(上面)
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
//这样让ProviderManager调用UserDetailsService类中的loadUserByUsername方法完成认证
//如果认证不通过,authenticate为null
//认证没通过,给出提示
if(Objects.isNull(authenticate)){
throw new RuntimeException("登陆失败");
}
//认证通过,使用userid生成jwt,jwt存入ResponseResult返回
//获取userId
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
String userId = loginUser.getUser().getId().toString();
String jwt = JwtUtil.createJWT(userId);
Map<String,String> map = new HashMap<>();
map.put("token",jwt);
//完整信息存入redis,userid作key
redisCache.setCacheObject("login:"+userId,loginUser);
return new ResponseResult(200,"登陆成功",map);
}
}
springSecurity流程图中是通过获取AuthenticationManager的authenticate方法进行认证。通过SecurityConfig中注入的bean获取AuthenticationManager。
authenticationManager的authenticate方法需要一个Authentication实现类参数,所以我们创建一个UsernamePasswordAuthenticationToken实现类
其中的JwtUtil.createJWT(userId);方法,是我自定义的根据userId生成JWT的工具类方法:
java
public class JwtUtil {
//有效期为
public static final Long JWT_TTL = 60*60*1000L;//一个小时
//设置密钥明文 。随便定义,方便记忆和使用即可,但需要长度要为4的倍数。
public static final String JWT_KEY = "jyue";
public static String getUUID(){
String token = UUID.randomUUID().toString().replaceAll("-", "");
return token;
}
//生成JWT
//subject为token中存放的数据(json格式)
public static String createJWT(String subject){
JwtBuilder builder = getJwtBuilder(subject, null, getUUID());//设置过期时间
return builder.compact();
}
public 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 expMills=nowMillis+ttlMillis;
Date expDate = new Date(expMills);
return Jwts.builder()
.setId(uuid) //唯一id
.setSubject(subject) //主题 可以是JSON数据
.setIssuer("jy") //签发者,随便写
.setIssuedAt(now) //签发时间
.signWith(signatureAlgorithm,secretKey) //使用HS256对称加密算法签名,第二个参数为密钥。
.setExpiration(expDate);
}
public static SecretKey generalKey(){
byte[] encodeKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
SecretKeySpec key = new SecretKeySpec(encodeKey, 0, encodeKey.length, "AES");
return key;
}
public static Claims parseJWT(String jwt)throws Exception{
SecretKey secretKey = generalKey();
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getBody();
}
}
redisCache也为自定义的redis工具类:
java
@SuppressWarnings(value = {"unchecked","rawtypes"})
@Component
public class RedisCache {
@Autowired
public RedisTemplate redisTemplate;
//缓存对象
//key 缓存键值
//value 缓存值
public <T> void setCacheObject(final String key,final T value){
redisTemplate.opsForValue().set(key,value);
}
//获取缓存的基本对象
// key 键值
// return 缓存键对应的数据
public <T>T getCacheObject(final String key){
ValueOperations<String,T> operation = redisTemplate.opsForValue();
return operation.get(key);
}
}
JWT认证:
java
@Component //继承这个实现类,保证了请求只会经过该过滤器一次
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private RedisCache redisCache;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//首先需要从请求头中获取token
String token = request.getHeader("token");
//判断token是否为Null
if(!StringUtils.hasText(token)) {
//token没有的话,直接放行,抛异常的活交给后续专门的过滤器。
filterChain.doFilter(request, response);
//响应时还会经过该过滤器一次,直接return,不能执行下面的解析token的代码。
return;
}
//如何不为空,解析token,获得了UserId
String userId;
try {
Claims claims = JwtUtil.parseJWT(token);
userId = claims.getSubject();
} catch (Exception e) {
e.printStackTrace();
//token格式异常,不是正经token
throw new RuntimeException("token非法");
}
//根据UserId查redis获取用户数据
String key = "login:"+userId;
LoginUser loginUser = redisCache.getCacheObject(key);
if(Objects.isNull(loginUser)){
throw new RuntimeException("用户未登录");
}
//然后封装Authentication对象存入SecurityContextHolder
// 因为后续的过滤器会从SecurityContextHolder中获取信息判断认证情况,而决定是否放行。
// 这里用UsernamePasswordAuthenticationToken三个参数的构造函数,是因为其能设置已认证的状态(因为已经从redis中获取了信息,确认是认证的了)
//第一个参数为用户信息,第三个参数为权限信息,目前还没获取,先填null
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null,null);
//默认SecurityContextHolder是ThreadLocal线程私有的,这也是为什么上面要用UsernamePasswordAuthenticationToken三个参数的构造方法
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(request,response);
}
}
这样登陆后用户发送请求,后端会先从请求头中获取token,然后解析出userId,然后从redis中查询该用户详细信息。然后把用户的详细信息存入UsernamePasswordAuthenticationToken三个参数的构造函数,是因为其能设置已认证的状态(因为已经从redis中获取了信息,确认是认证的了),然后把UsernamePasswordAuthenticationToken存入SecurityContextHolder。
因为后续的过滤器会从SecurityContextHolder中获取信息判断认证情况,而决定是否放行。