Jwt学习笔记

笔记来源:文章
作者:Dearmadman
链接:https://www.jianshu.com/p/576dbf44b2ae
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

一 jwt介绍

1、jwt是什么

Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。

2、老生常谈的东西

说起JWT,我们应该来谈一谈基于token的认证和传统的session认证的区别。

(1)传统的session认证

我们知道,http协议本身是一种无状态的协议,而这就意味着如果用户向我们的应用提供了用户名和密码来进行用户认证,那么下一次请求时,用户还要再一次进行用户认证才行,因为根据http协议,我们并不能知道是哪个用户发出的请求,所以为了让我们的应用能识别是哪个用户发出的请求,我们只能在服务器存储一份用户登录的信息,这份登录信息会在响应时传递给浏览器,告诉其保存为cookie,以便下次请求时发送给我们的应用,这样我们的应用就能识别请求来自哪个用户了,这就是传统的基于session认证。

但是这种基于session的认证使应用本身很难得到扩展,随着不同客户端用户的增加,独立的服务器已无法承载更多的用户,而这时候基于session认证应用的问题就会暴露出来.

(2)基于session认证所显露的问题

Session: 每个用户经过我们的应用认证之后,我们的应用都要在服务端做一次记录,以方便用户下次请求的鉴别,通常而言session都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大。

扩展性: 用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力。这也意味着限制了应用的扩展能力。

CSRF: 因为是基于cookie来进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。

(3)基于token的鉴权机制

基于token的鉴权机制类似于http协议也是无状态的,它不需要在服务端去保留用户的认证信息或者会话信息。这就意味着基于token认证机制的应用不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利。

流程上是这样的

  • 用户使用用户名密码来请求服务器
  • 服务器进行验证用户的信息
  • 服务器通过验证发送给用户一个token
  • 客户端存储token,并在每次请求时附送上这个token值
  • 服务端验证token值,并返回数据
    *这个token必须要在每次请求时传递给服务端,它应该保存在请求头里, 另外,服务端要支持CORS(跨来源资源共享)策略,一般我们在服务端这么做就可以了Access-Control-Allow-Origin:

(4)再来看看jwt优点

  • jwt基于json,非常方便解析
  • 可以在令牌中自定义丰富的内容,易扩展
  • 通过非对称加密算法及数字签名技术,jwt防止慕改
  • 资源服务器使用jwt可不依赖认证服务器即可完成授权

3、jwt到底是什么样子呢?

JWT是由三段信息构成的,将这三段信息文本用.链接一起就构成了Jwt字符串。就像这样:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

(1) 第一步分是Header

jwt的头部承载两部分信息:

  • 声明类型,这里是jwt
  • 声明加密的算法 通常直接使用 HMAC SHA256
{
  'typ': 'JWT',
  'alg': 'HS256'
}

然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分.

然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分.

(2)第二部分playload

载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分

  • 标准中注册的声明
  • 公共的声明
  • 私有的声明

标准中注册的声明 (建议但不强制使用) :

  • iss: jwt签发者
  • sub: jwt所面向的用户
  • aud: 接收jwt的一方
  • exp: jwt的过期时间,这个过期时间必须要大于签发时间
  • nbf: 定义在什么时间之前,该jwt都是不可用的.
  • iat: jwt的签发时间
  • jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

公共的声明 :
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.

私有的声明 :
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。

定义一个payload:

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

然后将其进行base64加密,得到Jwt的第二部分。

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

(3)第三部分signature

jwt的第三部分是一个签证信息,这个签证信息由三部分组成:

  • header (base64后的)
  • payload (base64后的)
  • secret

这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。

header (base64后的)
payload (base64后的)
secret
这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。

将这三部分用.连接成一个完整的字符串,构成了最终的jwt:

  eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。

4、怎么应用呢?

(1)一般是在请求头里加入Authorization,并加上Bearer标注:

一般是在请求头里加入Authorization,并加上Bearer标注:

服务端会验证token,如果验证通过就会返回相应的资源。整个流程就是这样的:
在这里插入图片描述

(2)写个代码试一下咯

1、创建我们的SpringBoot项目,导入依赖

        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>

2、编写测试类

@SpringBootTest
class Seucrity02ApplicationTests {

    @Test
    void contextLoads() {
        JwtBuilder jwtBuilder = Jwts.builder()
                //唯一Id{"id","888"}
                .setId("888")
                // 接受的用户{“sub”,“Rose”}
                .setSubject("Rose")
                // 签发时间{“签发时间”,”xx"}
                .setIssuedAt(new Date())
                //签名算法,及秘钥
                .signWith(SignatureAlgorithm.HS256,"xxxx");
        //签发token
        String token = jwtBuilder.compact();
        System.out.println(token);

//进行解析练习,对应我们的C部分是解析不了的,会直接乱码
        String[] strings = token.split("\\.");
        System.out.println(Base64Codec.BASE64.decodeToString(strings[0]));
        System.out.println(Base64Codec.BASE64.decodeToString(strings[1]));
        System.out.println(Base64Codec.BASE64.decodeToString(strings[2]));
    }

}

3、运行结果

eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiJSb3NlIiwiaWF0IjoxNjMyNzQ4OTgzfQ.3woYEgZurOIFHTmdGr6TWy-IZ0CFbZs2dqt8821DEKI
第一部分:{"alg":"HS256"}
第二部分:{"jti":"888","sub":"Rose","iat":1632748983}
第三步分:n��9���["�![f͝��<�P�

(3)token的验证解析

我们刚才已经创建token,在web应用中这个操作是由服务器端进行然后发给客户端,客户端在下次向服务器端发送请求时需要携带这个token,那服务器接到到这个token应该解析出这个token中的信息,根据这些信息查询数据库返回响应的结果。

(4)解析代码

@Test
    void contextLoadss() {
        String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiJSb3NlIiwiaWF0IjoxNjMyNzQ4OTgzfQ.3woYEgZurOIFHTmdGr6TWy-IZ0CFbZs2dqt8821DEKI";
        Claims jwtBuilder = (Claims)Jwts.parser()
                .setSigningKey("xxxx")
                .parse(token)
                .getBody();

        System.out.println("id= " + jwtBuilder.getId());
        System.out.println("sub= " + jwtBuilder.getSubject());
        System.out.println("iat= " + jwtBuilder.getIssuer());
    }

5、jwt过期效验

1、设置过期时间

    @Test
    void contextLoadsExp() {

        long now = System.currentTimeMillis();
        long exp = now + 60*1000;
        JwtBuilder jwtBuilder = Jwts.builder()
                //唯一Id{"id","888"}
                .setId("888")
                // 接受的用户{“sub”,“Rose”}
                .setSubject("Rose")
                // 签发时间{“签发时间”,”xx"}
                .setIssuedAt(new Date())
                //签名算法,及秘钥
                .signWith(SignatureAlgorithm.HS256,"xxxx")
                //设置过期时间
                .setExpiration(new Date(exp));
        //签发token
        String token = jwtBuilder.compact();
        System.out.println(token);

        String[] strings = token.split("\\.");
        System.out.println(Base64Codec.BASE64.decodeToString(strings[0]));
        System.out.println(Base64Codec.BASE64.decodeToString(strings[1]));
        System.out.println(Base64Codec.BASE64.decodeToString(strings[2]));

    }

2、取得过期时间

@Test
    void contextLoadssExp() {
        String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiJSb3NlIiwiaWF0IjoxNjMyNzkyNDMzLCJleHAiOjE2MzI3OTI0OTN9.nrvw_b5CthCVC-kaHbFsab0nH1YMBgmT5yji9Ps1Nj8";
        Claims jwtBuilder = (Claims)Jwts.parser()
                .setSigningKey("xxxx")
                .parse(token)
                .getBody();

        System.out.println("id= " + jwtBuilder.getId());
        System.out.println("sub= " + jwtBuilder.getSubject());
        System.out.println("iat= " + jwtBuilder.getIssuer());

        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        System.out.println("签发时间" + simpleDateFormat.format(jwtBuilder.getIssuer()));
        System.out.println("过期时间" + simpleDateFormat.format(jwtBuilder.getExpiration()));
        System.out.println("过期时间" + simpleDateFormat.format(new Date()));
    }

3、如果超过时间就报错

ExpiredJwtException: JWT expired at 2021-09-28T09:31:39Z. Current time: 2021-09-28T09:33:22Z, a difference of 103146 milliseconds.  Allowed clock skew: 0 milliseconds.

6、自定义申明格式

@Test
    void contextLoadsExp() {

        long now = System.currentTimeMillis();
        long exp = now + 60*1000;
        JwtBuilder jwtBuilder = Jwts.builder()
                //唯一Id{"id","888"}
                .setId("888")
                // 接受的用户{“sub”,“Rose”}
                .setSubject("Rose")
                // 签发时间{“签发时间”,”xx"}
                .setIssuedAt(new Date())
                //签名算法,及秘钥
                .signWith(SignatureAlgorithm.HS256,"xxxx")
                //自定义生名
                .claim("roles","admin")
                .claim("logo","xxx.jpg");
                //另外可以直接插入map形式申名
                //》addClaims(map)
        //签发token
        String token = jwtBuilder.compact();
        System.out.println(token);

        String[] strings = token.split("\\.");
        System.out.println(Base64Codec.BASE64.decodeToString(strings[0]));
        System.out.println(Base64Codec.BASE64.decodeToString(strings[1]));
        System.out.println(Base64Codec.BASE64.decodeToString(strings[2]));

    }

二、jwt在项目中的应用

(1)首先配置文件

#jwt存储的请求头
jwt.tokenHeder=Authorization
#配置秘钥
jwt.secret=yeb-secret
#JWT的超期时间(60*60*24)
jwt.expiration=604800
#jwt负载中拿到开头
jwt.tokenHead=Bearer

(2)jwt配置类

package com.mldn.seucrity02.config.security;

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.util.Date;
import java.util.HashMap;
import java.util.Map;

@Component
public class JwtTokenUtil {

    //创建两个常量,一个是用户名的key
    private static final String CLAIM_KEY_USERNAME = "sub";
    //创建两个常量,一个是创建时间的key
    private static final String CLAIM_KEY_CRATED = "created";
    //另外其他配置的key就通过value注解去拿
    @Value("${jwt.secret}")
    //秘钥
    private String secret;
    @Value("${jwt.expiration}")
    //失效时间
    private Long expiration;

    /**
     * 接下来会顺序性的写下去
     * 1.根据用户名去生成token
     * 2.根据用户名去拿token
     * 3.判断是否失效
     * 4.判断是否可以被刷新
     * 5.判断是否刷新
     */

    /**
     * 1 生成token
     * @return
     */

    public String generateToken(UserDetails userDetails) {
        Map<String,Object> claims = new HashMap<>();
        claims.put(CLAIM_KEY_USERNAME,userDetails.getUsername());
        claims.put(CLAIM_KEY_CRATED,new Date());
        //有了上面的荷载,就可以做我们的生成了
        return generateToken(claims);
    }

    /**
     * 根据荷载生成jwt token
     * @param claims
     * @return
     */
    private String generateToken(Map<String,Object> claims) {
        return Jwts.builder()
                .setClaims(claims)
                //设置失效时间
                .setExpiration(generateExpiration())
                //签名
                .signWith(SignatureAlgorithm.ES512,secret)
                .compact();
    }

    /**
     * 生成token失效时间
     * @return
     */
    private Date generateExpiration() {
        return new Date(System.currentTimeMillis() + this.expiration*1000);
    }


    /**
     * 2. 方法二,从token中获取登录用户名
     */
    public String getUsernameFromToken(String token) {
        String username;
        //用户名是放在荷载里面的,要先获取荷载
        try {
            Claims claims = getClaimsFormToken(token);
            username = claims.getSubject();
        } catch (Exception e) {
            username = null;
        }
        return username;
    }

    /**
     * 从token里面获取荷载
     */

    private Claims  getClaimsFormToken(String token) {
        Claims claims = null;
        try {
            claims = Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return claims;
    }


    /**
     * 3 判断token是否失效
     *      1. 这个有两种情况都是失效的,一种的token是过期的
     *      2. 第二种是用户名对不上
     */

    public boolean validateToken(String token, UserDetails userDetails) {
        String username = getUsernameFromToken(token);
        return username.equals(userDetails.getUsername()) && !isTokenExpiration(token);
    }

    /**
     * 判断是否失效
     * @param token
     * @return
     */
    private boolean isTokenExpiration(String token) {
        Date expireDate = getExpiredDateFromToken(token);
        //判断是否在这个时间前面expireDate.before(new Date());
        return expireDate.before(new Date());
    }


    /**
     * token中获取过期时间
     * @param token
     * @return
     */
    private Date getExpiredDateFromToken(String token) {
        Claims claims = getClaimsFormToken(token);
        Date expiration = claims.getExpiration();
        return expiration;
    }


    /**
     * 4. 判断token是否被刷新
     */
    public boolean canRefresh(String token) {
        return !isTokenExpiration(token);
    }

    /**
     * 5. 刷新token的方法
     */
    public String refreshToken(String token) {
        Claims claims = getClaimsFormToken(token);
        claims.put(CLAIM_KEY_CRATED,new Date());
        return generateToken(claims);
    }
}

如果你只是像要学jwt的基本知识,到这里基本就结束了,大家可以去做项目中练习了,但是这样我也有个集合SpringSecurity+jwt的大致验证流程的

有了上面的基础了,那拿到实战用撒

三、实际项目中

那我们来介绍一下在实际的项目中,登录流程,然后返回token,实际项目中,一般都在登录模块有下面步骤

1、咱们的认证流程是写什么

1、 肯定是要先获取用户名和密码进行数据库效验
2、效验不通过时,请从新输入
3、效验通过,发放token,返回给前端
4、前端存着令牌,以后请求都放在请求头中,进行请求。
5、验证令牌通过的话再有其他的请求通过,否则就不行了
6、前端就会得到用户的信息,比如头像呀,还有其他的。

-----分割线-----
7、这里就要开发我们的SpringSecurity的配置了

2、了解我们的登录逻辑

有了上面的第一步了解后,下面开发来了解我们的登录逻辑

(0)下面有两个类要了解一下

第一个,公共返回类

## 1、一般都有的公共返回数据类

```xml

/**
 * 公共返回对象
 *
 * @author zhoubin
 * @since 1.0.0
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class RespBean {
	private long code;
	private String message;
	private Object obj;

	/**
	 * 成功返回结果
	 * @param message
	 * @return
	 */
	public static RespBean success(String message){
		return new RespBean(200,message,null);
	}

	/**
	 * 成功返回结果
	 * @param message
	 * @param obj
	 * @return
	 */
	public static RespBean success(String message,Object obj){
		return new RespBean(200,message,obj);
	}

	/**
	 * 失败返回结果
	 * @param message
	 * @return
	 */
	public static RespBean error(String message){
		return new RespBean(500,message,null);
	}

	/**
	 * 失败返回结果
	 * @param message
	 * @param obj
	 * @return
	 */
	public static RespBean error(String message,Object obj){
		return new RespBean(500,message,obj);
	}
}

第二个,登录参数类

 */
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@ApiModel(value = "AdminLogin对象",description = "")
public class AdminLoginParam {
	@ApiModelProperty(value = "用户名",required = true)
	private String username;
	@ApiModelProperty(value = "密码",required = true)
	private String password;
}

(1)了解一下一般单点登录的后端逻辑吧

(1)首先前端在相对应的页面输入用户名和密码,然后点击登录后,这个就是个请求开发发到后端来了

随便列如下面的一个:
在这里插入图片描述
(2)请求来了后,就到我们写的controller里面的请求里面来了。

@Api(tags = "LoginController")
@Controller
public class LoginController {

    @Autowired
    private SysUserService sysUserService;


    @ApiOperation(value = "登录之后返回token")
    @RequestMapping("/login")
    public RespBean showLogin(AdminLoginParam adminLoginParam, HttpServletRequest request) {
        return sysUserService.login(adminLoginParam.getUsername(),adminLoginParam.getPassword(),request);
    }
}

(3)到了请求头后,然后就会去找这个请求里面的方法,然后去找对应的Service层进行验证。

public interface SysUserService {

    public RespBean login(String username, String password, HttpServletRequest request) ;
}



@Service
public class SysUserServiceImpl implements SysUserService {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Value("${jwt.tokenHead}")
    private String tokenHead;
    /**
     * 登录之后返回token
     * @param username
     * @param password
     * @param request
     * @return
     */
    public RespBean login(String username, String password, HttpServletRequest request) {
        UserDetails userDetails = userDetailsService.loadUserByUsername("username");
        if (null == userDetails|| !passwordEncoder.matches(password,userDetails.getPassword())) {
            return RespBean.error("用户名或者密码错误");
        }
        if (!userDetails.isEnabled()) {
            return RespBean.error("账号被禁用,请联系管理员");

        }
        //然后上面都通过后就可以发令牌了
        String token = jwtTokenUtil.generateToken(userDetails);
        Map<String,String> tokenMap = new HashMap<>();
        //返回的token
        tokenMap.put("token",token);
        //返回头部信息,为什么要返回呢?因到时候返回前端后,前端会携带这个进行效验,这个和上面的一样的
        tokenMap.put("tokenHead",tokenHead);
        return RespBean.success("登录成功",tokenMap);
    }
}

(4)验证通过,发放令牌,不通过不发放。代码也在上面。

(2)编写我们的继承了UserDetails的类

@Data
public class SysUser implements Serializable , UserDetails {
    /*主键*/
    private Long id;

    /*用户名*/
    private String username;

    /*密码*/
    private String password;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        //权限的先空着
        return null;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

(3)然后就来看我们的请求

在这里插入图片描述

(4)看看我们的Login请求

@Api(tags = "LoginController")
@Controller
public class LoginController {

    @Autowired
    private SysUserService sysUserService;


    @ApiOperation(value = "登录之后返回token")
    @RequestMapping("/login")
    public RespBean showLogin(AdminLoginParam adminLoginParam, HttpServletRequest request) {
        return sysUserService.login(adminLoginParam.getUsername(),adminLoginParam.getPassword(),request);
    }
}

(5)看一下Service层

@Service
public interface SysUserService {

    public RespBean login(String username, String password, HttpServletRequest request) ;
}


@Service
public class SysUserServiceImpl implements SysUserService {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Value("${jwt.tokenHead}")
    private String tokenHead;
    /**
     * 登录之后返回token
     * @param username
     * @param password
     * @param request
     * @return
     */
    public RespBean login(String username, String password, HttpServletRequest request) {
        UserDetails userDetails = userDetailsService.loadUserByUsername("username");
        if (null == userDetails|| !passwordEncoder.matches(password,userDetails.getPassword())) {
            return RespBean.error("用户名或者密码错误");
        }
        if (!userDetails.isEnabled()) {
            return RespBean.error("账号被禁用,请联系管理员");

        }
        //然后上面都通过后就可以发令牌了
        String token = jwtTokenUtil.generateToken(userDetails);
        Map<String,String> tokenMap = new HashMap<>();
        //返回的token
        tokenMap.put("token",token);
        //返回头部信息,为什么要返回呢?因到时候返回前端后,前端会携带这个进行效验,这个和上面的一样的
        tokenMap.put("tokenHead",tokenHead);
        return RespBean.success("登录成功",tokenMap);
    }
}

(6)发放令牌的过程

public RespBean login(String username, String password, HttpServletRequest request) {
        UserDetails userDetails = userDetailsService.loadUserByUsername("username");
        if (null == userDetails|| !passwordEncoder.matches(password,userDetails.getPassword())) {
            return RespBean.error("用户名或者密码错误");
        }
        if (!userDetails.isEnabled()) {
            return RespBean.error("账号被禁用,请联系管理员");

        }
        //然后上面都通过后就可以发令牌了
        String token = jwtTokenUtil.generateToken(userDetails);
        Map<String,String> tokenMap = new HashMap<>();
        //返回的token
        tokenMap.put("token",token);
        //返回头部信息,为什么要返回呢?因到时候返回前端后,前端会携带这个进行效验,这个和上面的一样的
        tokenMap.put("tokenHead",tokenHead);
        return RespBean.success("登录成功",tokenMap);
    }

(7)刷新用户,这一步也很重要

@Service
public class SysUserServiceImpl implements SysUserService {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Value("${jwt.tokenHead}")
    private String tokenHead;
    /**
     * 登录之后返回token
     * @param username
     * @param password
     * @param request
     * @return
     */
    public RespBean login(String username, String password, HttpServletRequest request) {
        //第一步,登录的逻辑,以后可以加入验证码
        UserDetails userDetails = userDetailsService.loadUserByUsername("username");
        if (null == userDetails|| !passwordEncoder.matches(password,userDetails.getPassword())) {
            return RespBean.error("用户名或者密码错误");
        }
        if (!userDetails.isEnabled()) {
            return RespBean.error("账号被禁用,请联系管理员");

        }
        //第二步,更新Security登录用户对象
        UsernamePasswordAuthenticationToken upat = new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(upat);
        
        //第三步,下面是生成token
        //然后上面都通过后就可以发令牌了
        String token = jwtTokenUtil.generateToken(userDetails);
        Map<String,String> tokenMap = new HashMap<>();
        //返回的token
        tokenMap.put("token",token);
        //返回头部信息,为什么要返回呢?因到时候返回前端后,前端会携带这个进行效验,这个和上面的一样的
        tokenMap.put("tokenHead",tokenHead);
        return RespBean.success("登录成功",tokenMap);
    }

3、再来看看一体的返回用户信息

有了上面的1、2两步后,我们的登录验证使用jwt就完成三分之二了,还有返回用户信息就最完美了

(1)在LoginController里面编写请求



    /**
     * 根据用户名获取用户信息
     * @param principal
     * @return
     */
    @GetMapping("/sysuser/info")
    //为什么能用这个参数
    //因为我们刚刚做的那个刷新security用户放入全局了,所有直接用就行了
    /*//第二步,更新Security登录用户对象
        UsernamePasswordAuthenticationToken upat = new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(upat);*/
    public SysUser getUserInfo(Principal principal) {
        //首先判断是不是为空,为空肯定有问题
        if (null == principal) {
            return null;
        }
        String username = principal.getName();

        //正常情况下可以直接返回return sysUserService.getSysUserUsername(username)
        // 处于对个人的保护,设置password为null
        SysUser SysUser = sysUserService.getSysUserUsername(username);
        SysUser.setPassword(null);
        return SysUser;
    }
}

(2)编写我们的SysUserService

@Service
public interface SysUserService {

    public RespBean login(String username, String password, HttpServletRequest request) ;

    SysUser getSysUserUsername(String username);
}



在SysUserService里面添加方法
    /**
     * 根据用户名获取信息
     * @param username
     * @return
     */
    @Override
    public SysUser getSysUserUsername(String username) {
        return sysUserMapper.selectOne(new QuerWrapper<SysUser>.eq("username",username,"enabled",true));
    }
}

4、既然都了解到这里了,继续了解退出登录

既然都了解到这里了,那我们就顺便了解一些退出登录的功能了吧,
1、退出登录大概流程
前端发送请求,后端接受请求后,然后进行响应的处理

  • 这个处理可以交由前端来处理(后端响应请求后,返回响应码给前端,前端删除相对于的Session就可以 了)
  • 这个响应也可以给后端来处理,这就是前后端分离的好处。

2、大概请求就是,以前端处理的形式

@PostMapping(“/logout”)
public RespBean logout() {
	//这个就相当于返回了一个响应码200回去
	returen RespBean.success("注销成功”);
}

5、中场小休息一下

利用中场休息我们来复习一下我们的SpringSecurity里面的配置方法

1、configure(WebSecurity web) ,//这个就是配置需要前端访问的一些资源,在这里不用验证就放行的

public void configure(WebSecurity web) throws Exception {
        //这个就是配置需要前端访问的一些资源,在这里不用验证就放行的
    }

2、configure(AuthenticationManagerBuilder auth),//这个方法是自定义的一些验证规则

@Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    
}

3、configure(HttpSecurity http),//这个就是SpringSecurity的安全配置

在这里插入代码片@Override
    protected void configure(HttpSecurity http) throws Exception {
    
}

四、编写SpringSecurity的配置类

这个第四部分也是项目中的一部分,为了重要性这里分一个块来讲解

1、我们来了解一些验证的配置

首先我们肯迪要编写这个Security的配置类,然后就是要注入我们的UserDetailsService这个进来,因为他承担了我们的用户的类。
第二部分就要编写我们自己的认证规则,里面的一些具体的配置
第三部分就是配置我们的jwt的使用

2、下面是具体的配置

(1)编写SecurityConfig

/**
 * Security配置类
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
   
}

(2)把我们SpringSecurity认证的类写进来

/**
 * Security配置类
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private SysUserService sysUserService;
    /**
     * SpringSecurity的登录逻辑主要是通过下面这个类实现的,所有先注入
     * 下面相当于是重写了,所有加上Override
     */

    @Override
    @Bean
    public UserDetailsService userDetailsService() {
        return username -> {
            SysUser admin = sysUserService.getSysUserUsername(username);
            if (null != admin) {
                return admin;
            }
            return null;
        };
    }
}

(3)下面来自定义认证规则

@Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf()
                //1. 这个csrf要使用disable()关闭,因为我们用jwt
                .disable()
                //2. 由于基于token,下面的也关闭
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                //3. http.authorizeRequests()方法有很多子方法,每个子匹配器将会按照声明的顺序起作用。
                .authorizeRequests()
                // 这个就是子方法允许登录
                .antMatchers("/login","/logout")
                .permitAll()
                //  其他的就要登录访问
                .anyRequest()
                .authenticated()
                .and()
                //缓存的用不到
                .headers()
                .cacheControl();


    }

(4)使用了jwt,就要配置过滤器

1、首先要在这里配置

//既然我们都是使用的jwt,肯定要配置拦截器
        http.addFilterBefore();
        //也可以定义其他没授权会到达的错误页面样式
        http.exceptionHandling()
                .accessDeniedHandler()
                .authenticationEntryPoint();

2、编写我们的过滤器

/**
 * JWT登录授权过滤器
 *
 * @author zhoubin
 * @since 1.0.0
 */
public class JwtAuthencationTokenFilter extends OncePerRequestFilter {

	@Value("${jwt.tokenHeader}")
	private String tokenHeader;
	@Value("${jwt.tokenHead}")
	private String tokenHead;
	@Autowired
	private JwtTokenUtil jwtTokenUtil;
	@Autowired
	private UserDetailsService userDetailsService;

	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
		String authHeader = request.getHeader(tokenHeader);
		//存在token
		if (null != authHeader && authHeader.startsWith(tokenHead)) {
			String authToken = authHeader.substring(tokenHead.length());
			String username = jwtTokenUtil.getUserNameFromToken(authToken);
			//token存在用户名但未登录
			if (null != username && null == SecurityContextHolder.getContext().getAuthentication()) {
				//登录
				UserDetails userDetails = userDetailsService.loadUserByUsername(username);
				//验证token是否有效,重新设置用户对象
				if (jwtTokenUtil.validateToken(authToken, userDetails)) {
					UsernamePasswordAuthenticationToken authenticationToken =
							new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
					authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
					SecurityContextHolder.getContext().setAuthentication(authenticationToken);
				}
			}

		}
		filterChain.doFilter(request, response);
	}
}

(5)然后就是自定义的那些未授权的页面

配置类一

/**
 * 当访问接口没有权限时,自定义返回结果
 *
 * @author zhoubin
 * @since 1.0.0
 */
@Component
public class RestfulAccessDeniedHandler implements AccessDeniedHandler {

	@Override
	public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
		response.setCharacterEncoding("UTF-8");
		response.setContentType("application/json");
		PrintWriter out = response.getWriter();
		RespBean bean = RespBean.error("权限不足,请联系管理员!");
		bean.setCode(403);
		out.write(new ObjectMapper().writeValueAsString(bean));
		out.flush();
		out.close();
	}
}

配置类二

**
 * 当未登录或者token失效时访问接口时,自定义的返回结果
 *
 * @author zhoubin
 * @since 1.0.0
 */
@Component
public class RestAuthorizationEntryPoint implements AuthenticationEntryPoint {

	@Override
	public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
		response.setCharacterEncoding("UTF-8");
		response.setContentType("application/json");
		PrintWriter out = response.getWriter();
		RespBean bean = RespBean.error("尚未登录,请登录!");
		bean.setCode(401);
		out.write(new ObjectMapper().writeValueAsString(bean));
		out.flush();
		out.close();
	}
}

(6)然后来看看我们总体的SecurityConfig


/**
 * Security配置类
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private SysUserService sysUserService;

    @Autowired
    private RestAuthorizationEntryPoint restAuthorizationEntryPoint;

    @Autowired
    private RestfulAccessDeniedHandler restfulAccessDeniedHandler;

    /**
     * 主要有三步
     * 1、注入我们的UserDetailsService
     * 2、配置自定义认证规则configure(AuthenticationManagerBuilder auth)
     * 3、SpringSecurity的安全配置
     */

    /**
     * 1、注入我们的UserDetailsService
     * SpringSecurity的登录逻辑主要是通过下面这个类实现的,所有先注入
     * 下面相当于是重写了,所有加上Override
     */
    @Override
    @Bean
    public UserDetailsService userDetailsService() {
        return username -> {
            SysUser admin = sysUserService.getSysUserUsername(username);
            if (null != admin) {
                return admin;
            }
            return null;
        };
    }

    /**
     * 2、配置自定义认证规则configure(AuthenticationManagerBuilder auth)
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // userDetailsService(userDetailsService())是下面注入的
        // 这个passwordEncoder导入下面注入的PasswordEncoder
        auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 3、SpringSecurity的安全配置
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf()
                //1. 这个csrf要使用disable()关闭,因为我们用jwt
                .disable()
                //2. 由于基于token,下面的也关闭
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                //3. http.authorizeRequests()方法有很多子方法,每个子匹配器将会按照声明的顺序起作用。
                .authorizeRequests()
                // 这个就是子方法允许登录
                .antMatchers("/login","/logout")
                .permitAll()
                //  其他的就要登录访问
                .anyRequest()
                .authenticated()
                .and()
                //缓存的用不到
                .headers()
                .cacheControl();

        //既然我们都是使用的jwt,肯定要配置拦截器
        http.addFilterBefore(jwtAuthencationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
        //也可以定义其他没授权会到达的错误页面样式
        http.exceptionHandling()
                .accessDeniedHandler(restfulAccessDeniedHandler)
                .authenticationEntryPoint(restAuthorizationEntryPoint);

    }

    @Bean
    public JwtAuthencationTokenFilter jwtAuthencationTokenFilter() {
        return new JwtAuthencationTokenFilter();
    }
}


总结:过滤器是要用bean注入的,其他的未授权页面是使用Autowired注入

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值