shuiluobu 2020-01-29
一丶 基本介绍
前后端分离的认证及授权有两种方式,
第一种是使用jwt 也就是(Json Web Token),客户端请求服务端,完成账号密码的认证以后,由服务端生成一个带有过期时间的token,返回给客户端,后续每次请求客户端都要带上这个token,服务端从请求中拿到token 进行解析 判断是否过期,然后构建spring security的安全对象,交由spring security框架进行后续的认证等处理.这种方式相比于传统的session方式不同,是无状态的,服务端没有保存和每个客户端对应的session对象,而是由客户端每次请求带上token,服务端进行解析 来判断客户端的身份,这相比传统方式对服务端的压力非常小,不需要保存和每个客户端对应的session对象,而且由于前后端分离,后端更加倾向于提供接口,很多业务逻辑前移,后端只需要认证请求的身份,提供好对应的接口,剩下的权限控制,跳转页面等就交由前端实现.
第二种 就是spring cloud的OAuth2认证方式,我这里没有去研究,所以就不细说了.
我这里也不过多介绍jwt了 百度相关的文章很多,我就直接介绍spring boot怎么整合jwt实现登录认证及授权
首先贴出maven依赖
<!-- jwt依赖 --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency> <!--spring security的依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId></dependency>
暂时就引入这两个依赖吧,一个是jwt,另外一个呢是spring security的依赖
二丶 代码实现
操作jwt生成token有一个现成的工具类,已经写好了常用方法,比如根据用户名生成token,计算token过期时间等方法,我这里先把这个工具类贴上来
import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Component; import java.io.Serializable; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.function.Function; /** * @Description: JwtTokenUtil,JWT工具类,生成/验证/是否过期token 。 * @Author: Tan * @CreateDate: 2019/12/2 **/ @Component public class JwtTokenUtil implements Serializable { private static final long serialVersionUID = -2550185165626007488L; //token有效期 @Value("${jwt.validity}") private Long tokenValidity; //加密秘钥 @Value("${jwt.secret}") private String secret; //通过token返回用户名 public String getUsernameFromToken(String token) { return getClaimFromToken(token, Claims::getSubject); } //通过token得到token过期时间 public Date getExpirationDateFromToken(String token) { return getClaimFromToken(token, Claims::getExpiration); } //从token中获得用户信息 public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) { final Claims claims = getAllClaimsFromToken(token); return claimsResolver.apply(claims); } //从token中解密 获得用户信息 private Claims getAllClaimsFromToken(String token) { return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody(); } //验证token是否过期 private Boolean isTokenExpired(String token) { final Date expiration = getExpirationDateFromToken(token); return expiration.before(new Date()); } //根据用户生成token public String generateToken(UserDetails userDetails) { Map<String, Object> claims = new HashMap<>(); return doGenerateToken(claims, userDetails.getUsername()); } //生成token private String doGenerateToken(Map<String, Object> claims, String subject) { return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis())) .setExpiration(new Date(System.currentTimeMillis() + (tokenValidity * 1000))) .signWith(SignatureAlgorithm.HS512, secret).compact(); } //验证token public Boolean validateToken(String token, UserDetails userDetails) { final String userName = getUsernameFromToken(token); return (userName.equals(userDetails.getUsername()) && !isTokenExpired(token)); } }
tokenValidity这个token有效期和secret加密秘钥,这两个变量是通过读取spring boot的application.yml配置文件中定义的,在spring Ioc容器实例化这个类的实例的时候就会从配置文件中读取,这样不写死,也方便后续修改到这里关于jwt的代码实现其实已经结束了,已经可以生成token,和验证token了,接下来就是关于spring security的配置部分了,其实spring security相比于另外一个安全框架shiro来说绝对算是重量级,也比较复杂,但是呢由于是spring提供,搭配整个spring生态使用应该还是可以的首先spring security对用户的操作,比如登录判断用户名密码是否正确,访问某个资源是否有对应的权限,定义了一个接口 或许也有类吧 但是我是实现了接口 重写了那些方法 就算是满足了spring security要求的安全用户对象,在它内部的实现机制就会用到,我们只需要传参构建好这个对象即可
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; /** * @Description: 实现 UserDetails 重写方法 就是满足spring security安全要求的用户 * spring security验证用户必须要使用实现UserDetails的类,的实例 * 所以构建这个类 将我们自身实体类中的一些字段 赋值到这个类 用于校验 * 也是由于我们自身的用户实体类 字段比较多 * @Author: Tan * @CreateDate: 2019/12/6 **/ public class SecurityUser implements UserDetails { //用户名 private String userName; //密码 private String passWord; //权限集合 private Collection<? extends GrantedAuthority> authoritys; //是否可用 private boolean enabled; /** * @Description: 这个构造方法 从用户实体对象中给这个安全用户赋值 * @Author: Tan * @Date: 2019/12/6 * @param userName: 用户账号 * @param passWord: 用户密码 * @param authority: 用户权限集合 * @param enabled: 用户是否可用 * @return: null **/ public SecurityUser (String userName, String passWord, List<String> authority,boolean enabled ){ this.userName=userName; this.passWord=passWord; this.enabled=enabled; this.authoritys =authority.stream().map(item->new SimpleGrantedAuthority(item)).collect(Collectors.toList()); } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return this.authoritys; } @Override public String getPassword() { return this.passWord; } @Override public String getUsername() { return this.userName; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return this.enabled; } }
这个类只定义了用户名,密码,拥有的权限和是否可用,其实还可以定义几个属性,比如该用户是否未锁定,是否未过期,密码是否未过期,这里我就没有写了 在重写的方法里面默认返回都是true,这个类写好了,在别的地方会实例化的.
其实为什么不使用和数据库对应的User实体类来实现这个接口,因为和数据库对应的User实体类肯定是有很多无关的字段,所以还是单独建一个类,将用户名,密码这些值传进来进行构建比较好.
接下来编写一个类实现一个接口重写一个方法,后续spring security框架会将得到的用户名调用这个方法,我们可以在这个方法里面将得到的用户名去数据库查询出是否有对应的记录,如果有记录就构建上面这个类的对象,传入用户名,密码,权限集合,是否可用然后返回
如果不存在这个记录,直接抛出一个用户名不存在异常 UsernameNotFoundException,我们这里返回了一个有用户名,密码,权限集合的对象,如果是登录的话,spring security框架会将这个用户名密码和从请求里面得到的token中解析出来的用户进行匹配 如果匹配失败
就会响应401错误,把代码贴出来
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.tqq.eggchat.dao.UserMapper; import com.tqq.eggchat.entity.SecurityUser; import com.tqq.eggchat.entity.User; import lombok.extern.slf4j.Slf4j; 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; import java.util.Arrays; /** * @Description: 这个类实现UserDetailsService接口 成为满足spring security标准的用户业务类 * 提供根据用户名 返回 UserDetails对象的方法 * 这里可以注入dao类对象 查询数据库 对应的用户信息 然后构造UserDetails对象 * @Author: Tan * @CreateDate: 2019/12/6 **/ @Slf4j @Service public class SecurityService implements UserDetailsService { @Autowired private UserMapper userMapper; /** * @Description: 根据用户名去数据库查询对应的用户信息 * @Author: Tan * @Date: 2019/12/6 * @param userName: 用户名 * @return: org.springframework.security.core.userdetails.UserDetails **/ @Override public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException { //根据用户名查询用户信息 User user = userMapper.selectOne(new QueryWrapper<User>().eq("s_account", userName)); if(user!=null){ //这里暂时没有权限的概念 就默认个user权限 return new SecurityUser(user.getS_account(),user.getS_password(), Arrays.asList("USER"),user.getI_status()==1?true:false); }else{ log.info("查询数据库,该账号{}不存在",userName); throw new UsernameNotFoundException(String.format("%s 该账号不存在",userName)); } } }
我这个@Slf4j是lombok框架提供的一个功能,相当于是一个日志对象,省得重复写了,直接在代码中就可以用,在编译以后会加上的.要想使用这个功能除了要引用lombok框架的依赖,使用的IDE也要装插件才能使用
客户端每次请求都会带上token.那么就需要一个过滤器,从请求对象中获取token 然后进行解析等,把过滤器代码贴出来
/** * @Description: 这个过滤器用于判断请求中是否有token 如果有就进行登录到spring security中 * 继承OncePerRequestFilter 这个类是spring 对filter的封装 可以实现 * 一次请求 只会执行一次过滤器 * @Author: Tan * @CreateDate: 2019/12/6 **/ @Component public class JwtRequestFilter extends OncePerRequestFilter { @Autowired private SecurityService securityService; @Autowired private JwtTokenUtil jwtTokenUtil; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String tokenHead=request.getHeader("Authorization"); //token头不等于空 并且以Bearer 开头进行token验证登录处理 if(tokenHead!=null&&tokenHead.startsWith("Bearer ")){ //从请求头中截取token String token=tokenHead.substring(7); //通过token得到用户名 如果token已过期或者错误 会抛出异常,并被spring security捕获 调我们自定义的登录失败处理方法 String userName = jwtTokenUtil.getUsernameFromToken(token); //用户名不等于空 并且当前上下文环境中没有认证过 就进行登录验证 if(userName!=null&& SecurityContextHolder.getContext().getAuthentication()==null){ //通过用户名查询数据库 构建符合spring security要求的安全用户对象 UserDetails userDetails = securityService.loadUserByUsername(userName); //验证token和用户对象 if(jwtTokenUtil.validateToken(token,userDetails)){ //通过安全用户对象 构建一个登录对象 UsernamePasswordAuthenticationToken login=new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities()); //传入当前http请求对象 login.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); //将登录对象 写入到当前上下文环境中 后续的判断 权限控制就由spring Security做 SecurityContextHolder.getContext().setAuthentication(login); } //调用下一个过滤器 如果有token已经在此完成登录 没有登录的话 会被后续拦截器处理 filterChain.doFilter(request,response); } }
在这个过滤器里面有注入了操作jwt的工具类和之前定义的用于根据用户名查询数据库构建spring security要求的用户对象的类,