学习目标:
能够使用BCrypt密码加密算法实现注册与登陆功能
能够说出常见的认证机制
能够说出JWT的组成部分,以及使用JWT的优点
能够使用JJWT 创建和解析token
能够使用JJWT完成微服务鉴权
任何应用考虑到安全,绝不能明文的方式保存密码。密码应该通过哈希算法进行加密。
有很多标准的算法比如SHA或者MD5,结合salt(盐)是一个不错的选择。 Spring Security
提供了BCryptPasswordEncoder类,实现Spring的PasswordEncoder接口使用BCrypt强
哈希方法来加密密码。
BCrypt强哈希方法 每次加密的结果都不一样。345
(1)tensquare_user工程的pom引入依赖
(2)添加配置类 (资源/工具类中提供)
/**
* 安全配置类
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/**").permitAll()
.anyRequest().authenticated()
.and().csrf().disable();
}
}
(3)修改tensquare_user工程的Application, 配置bean
修改tensquare_user工程的AdminService
@Autowired
BCryptPasswordEncoder encoder;
public void add(Admin admin) {
admin.setId(idWorker.nextId()+""); //主键值
//密码加密
String newpassword = encoder.encode(admin.getPassword());//加密后
的密码
admin.setPassword(newpassword);
adminDao.save(admin);
}
(1)AdminDao增加方法定义
public Admin findByLoginname(String loginname);
(2)AdminService增加方法
/**
* 根据登陆名和密码查询
* @param loginname
* @param password
* @return
*/
public Admin findByLoginnameAndPassword(String loginname, String
password){
Admin admin = adminDao.findByLoginname(loginname);
if( admin!=null && encoder.matches(password,admin.getPassword()))
{
return admin;
}else{
return null;
}
}
(3)AdminController增加方法
/**
* 用户登陆
* @param loginname
* @param password
* @return
*/
@RequestMapping(value="/login",method=RequestMethod.POST)
public Result login(@RequestBody Map loginMap){
Admin admin =
adminService.findByLoginnameAndPassword(loginMap.get("loginname"),
loginMap.get("password"));
if(admin!=null){
return new Result(true,StatusCode.OK,"登陆成功");
}else{
return new Result(false,StatusCode.LOGINERROR,"用户名或密码错
误");
}
}
(4)修改tensquare_user工程的UserService 类,引入BCryptPasswordEncoder
(5)修改tensquare_user工程的UserService 类的add方法,添加密码加密的逻辑
/**
* 增加
* @param user
* @param code
*/
public void add(User user,String code) {
........
........
........
//密码加密
String newpassword = encoder.encode(user.getPassword());//加密后的
密码
user.setPassword(newpassword);
userDao.save(user);
}
(1)修改tensquare_user工程的UserDao接口,增加方法定义
HTTP Basic Auth简单点说明就是每次请求API时都提供用户的username和
password,简言之,Basic Auth是配合RESTful API 使用的最简单的认证方式,只需提供
用户名密码即可,但由于有把用户名密码暴露给第三方客户端的风险,在生产环境下被
使用的越来越少。因此,在开发对外开放的RESTful API时,尽量避免采用HTTP Basic
Auth
Cookie认证机制就是为一次请求认证在服务端创建一个Session对象,同时在客户端
的浏览器端创建了一个Cookie对象;通过客户端带上来Cookie对象来与服务器端的
session对象匹配来实现状态管理的。默认的,当我们关闭浏览器的时候,cookie会被删
除。但可以通过修改cookie 的expire time使cookie在一定时间内有效;
OAuth(开放授权)是一个开放的授权标准,允许用户让第三方应用访问该用户在
某一web服务上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和
密码提供给第三方应用。
JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用
户和服务器之间传递安全可靠的信息。
一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。
头部(Header)
头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。这也可以
被表示成一个JSON对象。
载荷(playload)
载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包
含三个部分
(1)标准中注册的声明(建议但不强制使用)
iss: jwt签发者
sub: jwt所面向的用户
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
2)公共的声明
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.
但不建议添加敏感信息,因为该部分在客户端可解密.
(3)私有的声明
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64
是对称解密的,意味着该部分信息可以归类为明文信息。
这个指的就是自定义的claim。比如前面那个结构举例中的admin和name都属于自定的
claim。这些claim跟JWT标准规定的claim区别在于:JWT规定的claim,JWT的接收方在
拿到JWT之后,都知道怎么对这些标准的claim进行验证(还不知道是否能够验证);而
private claims不会验证,除非明确告诉接收方要对这些claim进行验证以及规则才行。
secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用
来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流
露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了
JJWT是一个提供端到端的JWT创建和验证的Java库。永远免费和开源(Apache
License,版本2.0),JJWT很容易使用和理解。它被设计成一个以建筑为中心的流畅界
面,隐藏了它的大部分复杂性。
(1)创建maven工程,引入依赖
io.jsonwebtoken
jjwt
0.6.0
(2)创建类CreateJwtTest,用于生成token
public class CreateJwtTest {
public static void main(String[] args) {
JwtBuilder builder= Jwts.builder().setId("888")
.setSubject("小白")
.setIssuedAt(new Date())
.signWith(SignatureAlgorithm.HS256,"itcast");
System.out.println( builder.compact() );
}
}
setIssuedAt用于设置签发时间
signWith用于设置签名秘钥
(3)测试运行,输出如下:
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1MjM0M
TM0NTh9.gq0J‐cOM_qCNqU_s‐d_IrRytaNenesPmqAIhQpYXHZk
我们刚才已经创建了token ,在web应用中这个操作是由服务端进行然后发给客户
端,客户端在下次向服务端发送请求时需要携带这个token(这就好像是拿着一张门票一
样),那服务端接到这个token 应该解析出token中的信息(例如用户id),根据这些信息
查询数据库返回相应的结果。
创建ParseJwtTest
public class ParseJwtTest {
public static void main(String[] args) {
String
token="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiO
jE1MjM0MTM0NTh9.gq0J‐cOM_qCNqU_s‐d_IrRytaNenesPmqAIhQpYXHZk";
Claims claims =
Jwts.parser().setSigningKey("itcast").parseClaimsJws(token).getBody();
System.out.println("id:"+claims.getId());
System.out.println("subject:"+claims.getSubject());
System.out.println("IssuedAt:"+claims.getIssuedAt());
}
}
试着将token或签名秘钥篡改一下,会发现运行时就会报错,所以解析token也就是验证
token
有很多时候,我们并不希望签发的token是永久生效的,所以我们可以为token添加一个
过期时间。
public class CreateJwtTest2 {
public static void main(String[] args) {
//为了方便测试,我们将过期时间设置为1分钟
long now = System.currentTimeMillis();//当前时间
long exp = now + 1000*60;//过期时间为1分钟
JwtBuilder builder= Jwts.builder().setId("888")
.setSubject("小白")
.setIssuedAt(new Date())
.signWith(SignatureAlgorithm.HS256,"itcast")
.setExpiration(new Date(exp));
System.out.println( builder.compact() );
}
}
setExpiration 方法用于设置过期时间
public class CreateJwtTest3 {
public static void main(String[] args) {
//为了方便测试,我们将过期时间设置为1分钟
long now = System.currentTimeMillis();//当前时间
long exp = now + 1000*60;//过期时间为1分钟
JwtBuilder builder= Jwts.builder().setId("888")
.setSubject("小白")
.setIssuedAt(new Date())
.signWith(SignatureAlgorithm.HS256,"itcast")
.setExpiration(new Date(exp))
.claim("roles","admin")
.claim("logo","logo.png");
System.out.println( builder.compact() );
}
}
(1)tensquare_common工程引入依赖(考虑到工具类的通用性)
(2)修改tensquare_common工程,创建util.JwtUtil
@ConfigurationProperties("jwt.config")
public class JwtUtil {
private String key ;
private long ttl ;//一个小时
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public long getTtl() {
return ttl;
}
public void setTtl(long ttl) {
this.ttl = ttl;
}
/**
* 生成JWT
*
* @param id
* @param subject
* @return
*/
public String createJWT(String id, String subject, String roles) {
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
JwtBuilder builder = Jwts.builder().setId(id)
.setSubject(subject)
.setIssuedAt(now)
.signWith(SignatureAlgorithm.HS256, key).claim("roles",
roles);
if (ttl > 0) {
builder.setExpiration( new Date( nowMillis + ttl));
}
return builder.compact();
}
/**
* 解析JWT
* @param jwtStr
* @return
*/
public Claims parseJWT(String jwtStr){
return Jwts.parser()
.setSigningKey(key)
.parseClaimsJws(jwtStr)
.getBody();
}
}
(3)修改tensquare_user工程的application.yml, 添加配置
(1)配置bean .修改tensquare_user工程Application类
@Bean
public JwtUtil jwtUtil(){
return new util.JwtUtil();
}
(2)修改AdminController的login方法
@Autowired
private JwtUtil jwtUtil;
/**
* 用户登陆
* @param loginname
* @param password
* @return
*/
@RequestMapping(value="/login",method=RequestMethod.POST)
public Result login(@RequestBody Map loginMap){
Admin admin =
adminService.findByLoginnameAndPassword(loginMap.get("loginname"),
loginMap.get("password"));
if(admin!=null){
//生成token
String token = jwtUtil.createJWT(admin.getId(),
admin.getLoginname(), "admin");
Map map=new HashMap();
map.put("token",token);
map.put("name",admin.getLoginname());//登陆名
return new Result(true,StatusCode.OK,"登陆成功",map);
}else{
return new Result(false,StatusCode.LOGINERROR,"用户名或密码错
误");
}
}
{
"flag": true,
"code": 20000,
"message": "登陆成功",
"data": {
"token":
"eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI5ODQzMjc1MDc4ODI5MzgzNjgiLCJzdWIiOiJ4aWF
vbWkiLCJpYXQiOjE1MjM1MjQxNTksInJvbGVzIjoiYWRtaW4iLCJleHAiOjE1MjM1MjQ1MTl9
._YF3oftRNTbq9WCD8Jg1tqcez3cSWoQiDIxMuPmp73o",
"name":"admin"
}
}
需求:删除用户,必须拥有管理员权限,否则不能删除。
前后端约定:前端请求微服务时需要添加头信息Authorization ,内容为Bearer+空格
+token
(1)修改UserController的findAll方法 ,判断请求中的头信息,提取token并验证权
限。
@Autowired
private HttpServletRequest request;
/**
* 删除
* @param id
*/
@RequestMapping(value="/{id}",method= RequestMethod.DELETE)
public Result delete(@PathVariable String id ){
String authHeader = request.getHeader("Authorization");//获取头信
息
if(authHeader==null){
return new Result(false,StatusCode.ACCESSERROR,"权限不足");
}
if(!authHeader.startsWith("Bearer ")){
return new Result(false,StatusCode.ACCESSERROR,"权限不足");
}
String token=authHeader.substring(7);//提取token
Claims claims = jwtUtil.parseJWT(token);
if(claims==null){
return new Result(false,StatusCode.ACCESSERROR,"权限不足");
}
if(!"admin".equals(claims.get("roles"))){
return new Result(false,StatusCode.ACCESSERROR,"权限不足");
}
userService.deleteById(id);
return new Result(true,StatusCode.OK,"删除成功");
}
如果我们每个方法都去写一段代码,冗余度太高,不利于维护,那如何做使我们的代码
看起来更清爽呢?我们可以将这段代码放入拦截器去实现
Spring为我们提供了
org.springframework.web.servlet.handler.HandlerInterceptorAdapter这个适配器,
继承此类,可以非常方便的实现自己的拦截器。他有三个方法:
分别实现预处理、后处理(调用了Service并返回ModelAndView,但未进行页面渲
染)、返回处理(已经渲染了页面)
在preHandle中,可以进行编码、安全控制等处理;
在postHandle中,有机会修改ModelAndView;
在afterCompletion中,可以根据ex是否为null判断是否发生了异常,进行日志记录
(1)创建拦截器类。创建 com.tensquare.user.filter.JwtFilter
@Component
public class JwtFilter extends HandlerInterceptorAdapter {
@Autowired
private JwtUtil jwtUtil;
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler)
throws Exception {
System.out.println("经过了拦截器");
return true;
}
}
(2)配置拦截器类,创建com.tensquare.user.ApplicationConfig
@Configuration
public class ApplicationConfig extends WebMvcConfigurationSupport {
@Autowired
private JwtFilter jwtFilter;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtFilter).
addPathPatterns("/**").
excludePathPatterns("/**/login");
}
}
(1)修改拦截器类 JwtFilter
@Component
public class JwtFilter extends HandlerInterceptorAdapter {
@Autowired
private JwtUtil jwtUtil;
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
System.out.println("经过了拦截器");
final String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
final String token = authHeader.substring(7); // The part
after "Bearer "
Claims claims = jwtUtil.parseJWT(token);
if (claims != null) {
if("admin".equals(claims.get("roles"))){//如果是管理员
request.setAttribute("admin_claims", claims);
}
if("user".equals(claims.get("roles"))){//如果是用户
request.setAttribute("user_claims", claims);
}
}
}
return true;
}
}
(2)修改UserController的delete方法
/**
* 删除
* @param id
*/
@RequestMapping(value="/{id}",method= RequestMethod.DELETE)
public Result delete(@PathVariable String id ){
Claims claims=(Claims) request.getAttribute("admin_claims");
if(claims==null){
return new Result(true,StatusCode.ACCESSRROR,"无权访问");
}
userService.deleteById(id);
return new Result(true,StatusCode.OK,"删除成功");
}
(2)修改UserController,引入JwtUtil 修改login方法 ,返回token,昵称,头像等信
息
@Autowired
private JwtUtil jwtUtil;
/**
* 用户登陆
* @param mobile
* @param password
* @return
*/
@RequestMapping(value="/login",method=RequestMethod.POST)
public Result login(@RequestBody Map loginMap){
User user =
userService.findByMobileAndPassword(loginMap.get("mobile"),loginMap.get("
password"));
if(user!=null){
String token = jwtUtil.createJWT(user.getId(),
user.getNickname(), "user");
Map map=new HashMap();
map.put("token",token);
map.put("name",user.getNickname());//昵称
map.put("avatar",user.getAvatar());//头像
return new Result(true,StatusCode.OK,"登陆成功",map);
}else{
return new Result(false,StatusCode.LOGINERROR,"用户名或密码错
误");
}
}
{
"flag": true,
"code": 20000,
"message": "登陆成功",
"data": {
"token":
"eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI5ODM5ODc0ODk1MDcyNTAxNzYiLCJpYXQiOjE1MjM
1MDIzMDEsInJvbGVzIjoidXNlciIsImV4cCI6MTUyMzUwMjY2MX0.QH‐
WMRgIAxHDcYReH7patg7qkcjc4ZuyDbHaIah‐spQ"
},
"name":"小雅",
"avatar":"......."
}
(1)修改tensquare_qa工程的QaApplication,增加bean
@Bean
public JwtUtil jwtUtil(){
return new util.JwtUtil();
}
(2)tensquare_qa工程配置文件application.yml增加配置:
jwt:
config:
key: itcast
(3)增加拦截器类 (参考上面的拦截器代码)
(4)增加配置类ApplicationConfig (参考上面的配置类代码)
(5)修改ProblemController的add方法
@Autowired
private HttpServletRequest request;
/**
* 发布问题
* @param problem
*/
@RequestMapping(method=RequestMethod.POST)
public Result add(@RequestBody Problem problem ){
Claims claims=(Claims)request.getAttribute("user_claims");
if(claims==null){
return new Result(false,StatusCode.ACCESSERROR,"无权访问");
}
problem.setUserid(claims.getId());
problemService.add(problem);
return new Result(true,StatusCode.OK,"增加成功");
}