一、BC加密(管理员例子)
1、准备工作
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
配置类
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//这个必写
.authorizeRequests()
//所有请求路径都运行
.antMatchers("/**").permitAll()
//所有请求都要经过验证
.anyRequest().authenticated()
//降低安全级别
.and().csrf().disable();
}
}
在启动类中加入bean
@SpringBootApplication
public class UserApplication {
public static void main(String[] args) {
SpringApplication.run(UserApplication.class, args);
}
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
}
2、新增管理员加密
这个就简单啦,在管理员注册加密就行
@Service
public class AdminService {
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
public void add(Admin admin) {
admin.setId( idWorker.nextId()+"" );
admin.setState("1");
//BC加密
admin.setPassword(bCryptPasswordEncoder.encode(admin.getPassword()));
adminDao.save(admin);
}
}
- admin.setPassword(bCryptPasswordEncoder.encode(admin.getPassword())); 进行BC加密
3、管理员登陆校验
一般来说,要登陆进去了,然后设置session或者token值,让前后端都知道通过验证了。
public interface AdminDao extends JpaRepository<Admin,String>,JpaSpecificationExecutor<Admin>{
public Admin findByLoginname(String loginname);
}
service
public Admin login(Admin admin) {
Admin adminLogin = adminDao.findByLoginname(admin.getLoginname());
//matches(输入的密码,数据库中的密码),可以让两个相匹配。因为密码通过BC每次都会不一样
if (adminLogin!=null && bCryptPasswordEncoder.matches(admin.getPassword(),adminLogin.getPassword())){
//数据库中找得到,然后密码一致,返回数据库中的admin
return adminLogin;
}
//否则就返回null
return null;
}
@PostMapping(value = "login")
public Result login(@RequestBody Admin admin){
Admin login = adminService.login(admin);
if (login!=null){
//这里还要写session或者token,后续
return new Result(true,StatusCode.OK,"登陆成功");
}else return new Result(false,StatusCode.LOGINERROR,"用户名或者密码错误");
}
- BC加密,每一次传过来同样的字符串都会生成不同的加密字符串,所有对比两者是否相同无法比较。
- 但是BC提供了一个方法我们进行匹配:
BC类.matches(输入的密码,数据库中的密码)
4、用户登陆注册
和上面的一样
注册
UserService
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
public void register(User user, String code) {
//..
user.setPassword(bCryptPasswordEncoder.encode(user.getPassword()));
//..
}
登陆
public interface UserDao extends JpaRepository<User,String>,JpaSpecificationExecutor<User>{
public User findByMobile(String mobile);
}
service
public User login(User user){
User userLogin = userDao.findByMobile(user.getMobile());
if (userLogin!=null && bCryptPasswordEncoder.matches(user.getPassword(),userLogin.getPassword())){
return userLogin;
}
return null;
}
controller
@PostMapping("login")
public Result login(@RequestBody User user){
User login = userService.login(user);
if (login!=null){
return new Result(true,StatusCode.OK,"登陆成功");
}else return new Result(false,StatusCode.LOGINERROR,"用户名或者密码错误");
}
二、常见的认证机制
1、HTTP Basic Auth
这是最原始的认证方式,输入username和password配合 RESTful API使用过。但是会把账号密码暴露,所以现在没什么人用这个。
2、Cookie Auth
这是就是登陆后存储一个session或者cookie,让前端知道已经登陆了。在关闭的时候cookie删除。但是安卓和IOS中没有cookie,不支持移动端。众所周知存储在cookie中是不安全的,会通过修改cookie和csrf攻击。
3、OAuth
第三方登陆,例如微信扫码登陆一样,无需输入任何账号和密码。
目前很流行,适用于个人消费者类的互联网产品如社交类APP,但是不适合拥有自有认证权限管理的企业应用
4、Token Auth
我们用的就是这个技术。
令牌登陆,登陆服务端生成一个令牌,给前端。前端收到后就知道登陆了。
流程:
- 客户端使用用户名跟密码请求登录
- 服务端收到请求,去验证用户名与密码
- 验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端
- 客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里
- 客户端每次向服务端请求资源的时候需要带着服务端签发的 Token
- 服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向 客户端返回请求的数据
优势:
- 支持跨域访问
- 无状态:token机制在服务端不需要存储session,不需要每一个用户都要记录,降低数据库压力
- CSRF:不依赖cookie,所以不需要进行SCRF防范
三、基于JWT的Token认证机制实现
JWT(JSON Web Token)
1、组成
其实际上就是一个字符串,由头部,载荷,签名三部分组成。
其中头部记录基本信息。载荷记录重要的用户信息之类的。
然后进行BASE64编码(因为JWT只支持BASE64编码)。将头部和载荷进行BASE64编码后连接成字符串,然后通过头部中声明的加密方式(HS256)通过加盐组合加密,构成签名。
然后将BASE64之后的头部和载荷,还有签名,通过.
进行连接,组成了JWT
头部
用于描述改JWT的基本信息
{"typ":"JWT","alg":"HS256"}
表明该类型是JWT,签名算法用HS256算法(哈希256)
然后进行BASE64编码eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
载荷
标准中注册的声明(推荐但不强制)
- iss: jwt签发者
- sub: jwt所面向的用户 ,一般写用户名
- aud: 接收jwt的一方
- exp: jwt的过期时间,这个过期时间必须要大于签发时间
- nbf: 定义在什么时间之前,该jwt都是不可用的.
- iat: jwt的签发时间
- jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
比如定义载荷{"sub":"1234567890","name":"John Doe","admin":true}
,BASE64后eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
签证
加密后的头部和载荷用.
连接的字符串,通过HS256
加密方式进行加盐组合(该盐一般公司内定,不让别人知道)加密
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
HS256加密TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
JWT
加起来就是
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
这么一串东西就是JWT
四、 Java的JJWT实现JWT
JJWT(Java JSON Web Token)
需要导包
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
1、快速入门
1)Jwt创建测试
public class CreateJwtTest {
public static void main(String[] args) {
JwtBuilder jwtBuilder = Jwts.builder()
.setId("666")//ID
.setSubject("小强")//用户名
.setIssuedAt(new Date())//创建时间
//signWith(加密算法,加密盐)
.signWith(SignatureAlgorithm.HS256, "tensquare");
//jwtBuilder.compact() 转化为String类型
System.out.println(jwtBuilder.compact());
}
}
setxxx()
,这就是载荷的那些东西,本来是填JSON但现在用Java写,自动转换signWith
头部,第一参数是加密方式,第二参数是加密盐值
,一般这个加密盐值
不让别人知道- 然后签名就会自动生成了
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI2NjYiLCJzdWIiOiLlsI_lvLoiLCJpYXQiOjE2MDY0NjQ2NDN9.rBjStx3MRdZ4bfOcu2iTnvnUE3TZ_0Oq7rtaMPCZ_yM
这就是三部分的东西,生成一个JWT了,然后每一次结果会不一样,因为加入了时间
2)解析Jwt
public class ParseJwtTest {
public static void main(String[] args) {
String Jwt = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI2NjYiLCJzdWIiOiLlsI_lvLoiLCJpYXQiOjE2MDY0NjUwODl9.eK0m9-39DF_fOjzgMqOQdaSiu9-uZmncYSn404JKAkY";
Claims claims = Jwts.parser()//开始解析
.setSigningKey("tensquare")//加密盐
.parseClaimsJws(Jwt)//那串JWT
.getBody();
System.out.println("用户ID:"+claims.getId());
System.out.println("用户名:"+claims.getSubject());
System.out.println("登陆时间:"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(claims.getIssuedAt()));
}
}
- Claims 类似一个Map
用户ID:666
用户名:小强
登陆时间:2020-11-27 16:18:09
3)JWT过期校验
public class CreateJwtTest {
public static void main(String[] args) {
//获取当前时间戳
long now = System.currentTimeMillis();
//过期时间,加一分钟
long exp = now + 1*60*1000;
JwtBuilder jwtBuilder = Jwts.builder()//开始构建
.setId("666")//ID
.setSubject("小强")//用户名
.setIssuedAt(new Date(now))//创建时间
.setExpiration(new Date(exp))//过期时间
//signWith(加密算法,加密盐)
.signWith(SignatureAlgorithm.HS256, "tensquare");
//jwtBuilder.compact() 转化为String类型
System.out.println(jwtBuilder.compact());
}
}
public class ParseJwtTest {
public static void main(String[] args) {
String Jwt = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI2NjYiLCJzdWIiOiLlsI_lvLoiLCJpYXQiOjE2MDY0Njc4MzgsImV4cCI6MTYwNjQ2Nzg5OH0.X-20hF-1xqrv6KZ0OLyUYrHrTTGTrbpCIkWI0tHV15w";
Claims claims = Jwts.parser()//开始解析
.setSigningKey("tensquare")//加密盐
.parseClaimsJws(Jwt)//那串JWT
.getBody();
System.out.println("用户ID:"+claims.getId());
System.out.println("用户名:"+claims.getSubject());
System.out.println("登陆时间:"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(claims.getIssuedAt()));
System.out.println("过期时间:"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(claims.getExpiration()));
}
}
用户ID:666
用户名:小强
登陆时间:2020-11-27 17:03:58
过期时间:2020-11-27 17:04:58
如果时间过期了
Exception in thread "main" io.jsonwebtoken.ExpiredJwtException: JWT expired at 2020-11-27T17:04:58Z. Current time: 2020-11-27T17:05:44Z, a difference of 46265 milliseconds. Allowed clock skew: 0 milliseconds.
at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:385)
at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:481)
at io.jsonwebtoken.impl.DefaultJwtParser.parseClaimsJws(DefaultJwtParser.java:541)
at jwt.ParseJwtTest.main(ParseJwtTest.java:14)
4)自定义创建键值
public class CreateJwtTest {
public static void main(String[] args) {
//获取当前时间戳
long now = System.currentTimeMillis();
//过期时间,加一分钟
long exp = now + 1*60*1000;
JwtBuilder jwtBuilder = Jwts.builder()//开始构建
.setId("666")//ID
.setSubject("小强")//用户名
.setIssuedAt(new Date(now))//创建时间
.setExpiration(new Date(exp))//过期时间
.claim("roles","user")//添加自定义键值
//signWith(加密算法,加密盐)
.signWith(SignatureAlgorithm.HS256, "tensquare");
//jwtBuilder.compact() 转化为String类型
System.out.println(jwtBuilder.compact());
}
}
public class ParseJwtTest {
public static void main(String[] args) {
String Jwt = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI2NjYiLCJzdWIiOiLlsI_lvLoiLCJpYXQiOjE2MDY0NjgyNzAsImV4cCI6MTYwNjQ2ODMzMCwicm9sZXMiOiJ1c2VyIn0.yWPMXOPcxPYvbjwkmOSUdzpGZJXpOsPJaqNjMHwpsFw";
Claims claims = Jwts.parser()//开始解析
.setSigningKey("tensquare")//加密盐
.parseClaimsJws(Jwt)//那串JWT
.getBody();
System.out.println("用户ID:"+claims.getId());
System.out.println("用户名:"+claims.getSubject());
System.out.println("登陆时间:"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(claims.getIssuedAt()));
System.out.println("过期时间:"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(claims.getExpiration()));
System.out.println("用户角色:"+claims.get("roles"));//claims.get("下标")
}
}
用户ID:666
用户名:小强
登陆时间:2020-11-27 17:11:10
过期时间:2020-11-27 17:12:10
用户角色:user
五、微服务鉴权
1、准备
记得tensquare_common导jjwt包
在tensquare_commom中添加工具类
@ConfigurationProperties("jwt.config")
@Data
public class JwtUtil {
private String key ;
private long ttl ;//一个小时
/**
* 生成JWT
*
* @param id 用户ID
* @param subject 用户名
* @param roles 角色名
* @return JWT字符串
*/
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()
//key就是盐值
.setSigningKey(key)
//JWT字符串
.parseClaimsJws(jwtStr)
.getBody();
}
}
写好工具类后,在user模块修改yml和注入Bean
jwt:
config:
# 盐值
key: tensquare
# 一个小时
ttl: 3600000
@Bean
public JwtUtil jwtUtil(){
return new JwtUtil();
}
2、管理员控制类获取token
然后在admin控制类修改
@RestController
@CrossOrigin
@RequestMapping("/admin")
public class AdminController {
@Autowired
private AdminService adminService;
@Autowired
private JwtUtil jwtUtil;
@PostMapping(value = "login")
public Result login(@RequestBody Admin admin){
Admin login = adminService.login(admin);
if (login!=null){
//这里写token
//生成token
String token = jwtUtil.createJWT(login.getId(), login.getLoginname(), "admin");//角色写死admin了
Map<String,Object> map = new HashMap<>();
map.put("token",token);
map.put("name",login.getLoginname());
//角色写死admin了
map.put("role","admin");
return new Result(true,StatusCode.OK,"登陆成功",map);
}else return new Result(false,StatusCode.LOGINERROR,"用户名或者密码错误");
}
这里给名字和权限是为了方便。
3、删除用户功能鉴权
删除用户,必须要又管理员权限,否则不能删除
把token放在body中不安全(我也不知道为啥不安全。。。)。所以把token放在消息头里面好一点
那些Response Headers就是消息头。
前后端约定:前端请求微服务时需要添加头信息Authorization
,内容为Bearer+空格+token
userService
@Service
public class UserService {
//消息头
@Autowired
private HttpServletRequest request;
@Autowired
private JwtUtil jwtUtil;
/**
* 删除需要具有管理员权限,权限放在消息头里面 Bearer+空格+token
* 删除
* @param id
*/
public void deleteById(String id) {
//获取key为Authorization的消息头
String authHeader = request.getHeader("Authorization");
//如果没有找到该消息头
if (authHeader==null){
throw new RuntimeException("权限不足");
}
//不以"Bearer "开头的话
if (!authHeader.startsWith("Bearer ")){
throw new RuntimeException("权限不足");
}
//从第7位直最后一位开始判断token,因为"Bearer "占7位,从0开始的
String token = authHeader.substring(7);
try {
//将token转义
Claims claims = jwtUtil.parseJWT(token);
//得到角色
String roles = (String) claims.get("roles");
//如果角色为空或者角色不是admin,异常
if (roles==null || !roles.equals("admin")){
throw new RuntimeException("权限不足");
}
}catch (Exception e){
//有啥异常都说权限不足了
throw new RuntimeException("权限不足");
}
//如果到这里都没错误的话
userDao.deleteById(id);
}
测试一下错误的
测试一下正确的
六、拦截器方式实现token鉴权
一个删除功能,就要写这么长得token鉴权代码了,其他得地方也是复制粘贴,还不如做拦截器好点。
这个拦截器呢,也不是真的拦截啦(还没到之后的访问无权限一步)。对所有请求路径进行判断,但都会放行。如果有令牌就记录在头消息,没有就不记录。所以只是做个token处理
1、测试拦截器
好久没做拦截器了,试下
拦截器类
//JWT令牌拦截器,对所有进行判断,但都会放行。如果有令牌就记录在头消息,没有就不记录
@Component
public class JwtInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("该路径经过了拦截器");
return true;//都放行
}
}
配置类,注册拦截器
//拦截器配置类,进行拦截器注册
@Configuration
public class InterceptorConfig extends WebMvcConfigurationSupport {
@Autowired
JwtInterceptor jwtInterceptor;
//添加拦截器
@Override
protected void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtInterceptor)//注册拦截器
.addPathPatterns("/**")//对所有路径拦截
.excludePathPatterns("/**/login/**");//但对登陆不拦截
}
}
localhost:9008/user查看全部用户,后台显示
该路径经过了拦截器
那么接下来就是在拦截器类中写代码了
2、拦截器验证token
//JWT令牌拦截器,对所有进行判断,但都会放行。如果有令牌就记录在头消息,没有就不记录
@Component
public class JwtInterceptor implements HandlerInterceptor {
@Autowired
JwtUtil jwtUtil;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String header = request.getHeader("Authorization");//获取头信息
//如果真的有这个头消息,就进行判定。如果有这个令牌,并且通过了一些列判断,再设置一个消息头键值claims_xxx
// 然后service那边就getAttribute(claims_xxx)进行判断,而不是还是用Authorization判断
if (header!=null && !"".equals(header)){
//如果头消息以Bearer 开头的话
if (header.startsWith("Bearer ")){
String token = header.substring(7);
try {
//将token转义
Claims claims = jwtUtil.parseJWT(token);
//得到角色
String roles = (String) claims.get("roles");
//判断角色是啥
if (roles!=null && roles.equals("admin")){
request.setAttribute("claims_admin",token);
}
if (roles!=null && roles.equals("user")){
request.setAttribute("claims_user",token);
}
}catch (Exception e){
throw new RuntimeException("令牌有误");
}
}
}
return true;//都放行
}
}
修改UserSerivce
public void deleteById(String id) {
//判断是否有claims_admin的消息头
String token = (String) request.getAttribute("claims_admin");
System.out.println(token);
//如果为空
if (token==null || "".equals(token)){
throw new RuntimeException("权限不足");
}
userDao.deleteById(id);
}
七、其他模块
完成了管理员登陆得到令牌,以及删除ID时需要令牌得到验证,以及拦截器。
然后其他模块也是复制粘贴就行
1、用户登陆添加令牌
@RestController
@CrossOrigin
@RequestMapping("/user")
public class UserController {
@Autowired
private JwtUtil jwtUtil;
@PostMapping("login")
public Result login(@RequestBody User user){
User login = userService.login(user);
if (login!=null){
String token = jwtUtil.createJWT(login.getId(), login.getMobile(), "user");//角色写死admin了
Map<String,Object> map = new HashMap<>();
map.put("token",token);
map.put("role","user");
return new Result(true,StatusCode.OK,"登陆成功");
}else return new Result(false,StatusCode.LOGINERROR,"用户名或者密码错误");
}
2、其他模块添加yml,和添加JWT的Bean
JWT工具类都写在common中,但是盐什么的还没写
jwt:
config:
# 盐值
key: tensquare
# 一个小时
ttl: 3600000
@Bean
public JwtUtil jwtUtil(){
return new JwtUtil();
}
3、其他模块添加拦截器
老复制粘贴了
JwtInterceptor和InterceptorConfig
4、在每一个需要权限的service层,添加token验证
比如什么添加问答,需要user权限
@Service
public class xxxService {
//消息头
@Autowired
private HttpServletRequest request;
@Autowired
private JwtUtil jwtUtil;
public void add(Problem problem) {
//判断是否有claims_admin的消息头
String token = (String) request.getAttribute("claims_user");//要么claims_user,要么claims_admin
//如果为空
if (token==null || "".equals(token)){
throw new RuntimeException("权限不足");
}
problem.setId( idWorker.nextId()+"" );
problemDao.save(problem);
}
5、测试
未登录
登陆:
header和body不能一起截图
八、整个流程
注册:
-
用户注册,后台随机生成6位数字,向redis缓存中保存一份,方便用户输入验证码时,从redis中获取数据进行对比。
-
然后利用RabbitMQ,注册时生产消息,发送给队列sms。然后设置消息消费者,队列sms一收到消息,就进行业务操作:利用阿里云短信功能,接受到队列消息,给目标手机号发送验证码信息。
-
用户手机接受到验证码后,输入个人信息和输入验证码。其中,密码用到BC加密(security包中)。后台接受数据后,从redis中查询对应的验证码。如果一样,则保存记录到MySQL,如果不一致,返回错误
登陆:
- 用户输入账号与密码,密码采用自带的函数进行验证,如果密码正确,则返回一个token令牌
- token具有时效性和权力性,执行任何增删改查操作都需要用到令牌。
增删改查操作
- 先进行一个拦截器,除了登陆那一步不拦截,所有的都拦截。
- 拦截器里面对所有请求都通行,但是都检查一下头消息中有没有带token(JWT)令牌。如果有,则将内容保留到一个新的头消息中;没有就没有,但也放行
- 进行业务增删改查操作时,需要检查一下头消息中有没有验证后的token,如果有,则进行操作;如果没有,就返回错误