23.认证服务-登录功能
普通登录
Member和auth服务里各放一份vo
@Data public class UserLoginVo { private String loginacct; private String password; }
@Override public MemberEntity login(MemberLoginVo memberLoginVo) { String loginacct = memberLoginVo.getLoginacct(); String password = memberLoginVo.getPassword(); //去数据库查询 loginacct有可能是用户名也有可能是手机号 MemberEntity memberEntity = baseMapper.selectOne(new QueryWrapper<MemberEntity>().eq("mobile", loginacct) .or().eq("username", loginacct)); //如果实体类不存在就登录失败 if(memberEntity==null){ return null; }else{ String passwordDB = memberEntity.getPassword(); //密码匹配 BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(); boolean matches = bCryptPasswordEncoder.matches(password, passwordDB); if(matches){ return memberEntity; } return null; } }
@PostMapping("/login") public R login(@RequestBody MemberLoginVo memberLoginVo){ MemberEntity memberEntity=memberService.login(memberLoginVo); if(memberEntity!=null){ return R.ok(); }else{ return R.error(BizCodeEnum.LOGIN_INVALID_EXCEPTION.getCode(),BizCodeEnum.LOGIN_INVALID_EXCEPTION.getMsg()); } }
远程调用接口
@PostMapping("/login") public String login(UserLoginVo vo,RedirectAttributes redirectAttributes){//页面提交过来的数据是key-value,不能用requestBody接受 R login = memberFeignService.login(vo); Map<String,String> errors=new HashMap<>(); if(login.getCode()==0){ return "redirect:http://gulimall.com"; }else{ //登录失败 errors.put("msg",login.getData("msg",new TypeReference<String>(){})); redirectAttributes.addFlashAttribute("errors",errors); return "redirect:http://auth.gulimall.com/login.html"; } }
社交登录--QQ,微信登录
OAuth(开放授权)是一个开放标准,允许用户授权第三方移动应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方移动应用或分享他们数据的所有内容,OAuth2.0是OAuth协议的延续版本,但不向后兼容OAuth 1.0即完全废止了OAuth1.0。
1、使用Code换取AccessToken,Code只能用一次
2、同一个用户的accessToken一段时间是不会变化的,即使多次获取
微信:https://developers.weixin.qq.com/doc/oplatform/Mobile_App/WeChat_Login/Development_Guide.html
客户端是
资源拥有者:用户本人
授权服务器:QQ服务器,微信服务器等。返回访问令牌
资源服务器:拿着令牌访问资源服务器看令牌合法性
Gitee第三方登录
HttpUtils这个类挺好用的
import org.apache.commons.lang.StringUtils; import org.apache.http.HttpResponse; import org.apache.http.NameValuePair; import org.apache.http.client.HttpClient; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpDelete; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPut; import org.apache.http.conn.ClientConnectionManager; import org.apache.http.conn.scheme.Scheme; import org.apache.http.conn.scheme.SchemeRegistry; import org.apache.http.conn.ssl.SSLSocketFactory; import org.apache.http.entity.ByteArrayEntity; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.message.BasicNameValuePair; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.List; import java.util.Map; public class HttpUtils { /** * get * * @param host * @param path * @param method * @param headers * @param querys * @return * @throws Exception */ public static HttpResponse doGet(String host, String path, String method, Map<String, String> headers, Map<String, String> querys) throws Exception { HttpClient httpClient = wrapClient(host); HttpGet request = new HttpGet(buildUrl(host, path, querys)); for (Map.Entry<String, String> e : headers.entrySet()) { request.addHeader(e.getKey(), e.getValue()); } return httpClient.execute(request); } /** * post form * * @param host * @param path * @param method * @param headers * @param querys * @param bodys * @return * @throws Exception */ public static HttpResponse doPost(String host, String path, String method, Map<String, String> headers, Map<String, String> querys, Map<String, String> bodys) throws Exception { HttpClient httpClient = wrapClient(host); HttpPost request = new HttpPost(buildUrl(host, path, querys)); for (Map.Entry<String, String> e : headers.entrySet()) { request.addHeader(e.getKey(), e.getValue()); } if (bodys != null) { List<NameValuePair> nameValuePairList = new ArrayList<NameValuePair>(); for (String key : bodys.keySet()) { nameValuePairList.add(new BasicNameValuePair(key, bodys.get(key))); } UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(nameValuePairList, "utf-8"); formEntity.setContentType("application/x-www-form-urlencoded; charset=UTF-8"); request.setEntity(formEntity); } return httpClient.execute(request); } /** * Post String * * @param host * @param path * @param method * @param headers * @param querys * @param body * @return * @throws Exception */ public static HttpResponse doPost(String host, String path, String method, Map<String, String> headers, Map<String, String> querys, String body) throws Exception { HttpClient httpClient = wrapClient(host); HttpPost request = new HttpPost(buildUrl(host, path, querys)); for (Map.Entry<String, String> e : headers.entrySet()) { request.addHeader(e.getKey(), e.getValue()); } if (StringUtils.isNotBlank(body)) { request.setEntity(new StringEntity(body, "utf-8")); } return httpClient.execute(request); } /** * Post stream * * @param host * @param path * @param method * @param headers * @param querys * @param body * @return * @throws Exception */ public static HttpResponse doPost(String host, String path, String method, Map<String, String> headers, Map<String, String> querys, byte[] body) throws Exception { HttpClient httpClient = wrapClient(host); HttpPost request = new HttpPost(buildUrl(host, path, querys)); for (Map.Entry<String, String> e : headers.entrySet()) { request.addHeader(e.getKey(), e.getValue()); } if (body != null) { request.setEntity(new ByteArrayEntity(body)); } return httpClient.execute(request); } /** * Put String * @param host * @param path * @param method * @param headers * @param querys * @param body * @return * @throws Exception */ public static HttpResponse doPut(String host, String path, String method, Map<String, String> headers, Map<String, String> querys, String body) throws Exception { HttpClient httpClient = wrapClient(host); HttpPut request = new HttpPut(buildUrl(host, path, querys)); for (Map.Entry<String, String> e : headers.entrySet()) { request.addHeader(e.getKey(), e.getValue()); } if (StringUtils.isNotBlank(body)) { request.setEntity(new StringEntity(body, "utf-8")); } return httpClient.execute(request); } /** * Put stream * @param host * @param path * @param method * @param headers * @param querys * @param body * @return * @throws Exception */ public static HttpResponse doPut(String host, String path, String method, Map<String, String> headers, Map<String, String> querys, byte[] body) throws Exception { HttpClient httpClient = wrapClient(host); HttpPut request = new HttpPut(buildUrl(host, path, querys)); for (Map.Entry<String, String> e : headers.entrySet()) { request.addHeader(e.getKey(), e.getValue()); } if (body != null) { request.setEntity(new ByteArrayEntity(body)); } return httpClient.execute(request); } /** * Delete * * @param host * @param path * @param method * @param headers * @param querys * @return * @throws Exception */ public static HttpResponse doDelete(String host, String path, String method, Map<String, String> headers, Map<String, String> querys) throws Exception { HttpClient httpClient = wrapClient(host); HttpDelete request = new HttpDelete(buildUrl(host, path, querys)); for (Map.Entry<String, String> e : headers.entrySet()) { request.addHeader(e.getKey(), e.getValue()); } return httpClient.execute(request); } private static String buildUrl(String host, String path, Map<String, String> querys) throws UnsupportedEncodingException { StringBuilder sbUrl = new StringBuilder(); sbUrl.append(host); if (!StringUtils.isBlank(path)) { sbUrl.append(path); } if (null != querys) { StringBuilder sbQuery = new StringBuilder(); for (Map.Entry<String, String> query : querys.entrySet()) { if (0 < sbQuery.length()) { sbQuery.append("&"); } if (StringUtils.isBlank(query.getKey()) && !StringUtils.isBlank(query.getValue())) { sbQuery.append(query.getValue()); } if (!StringUtils.isBlank(query.getKey())) { sbQuery.append(query.getKey()); if (!StringUtils.isBlank(query.getValue())) { sbQuery.append("="); sbQuery.append(URLEncoder.encode(query.getValue(), "utf-8")); } } } if (0 < sbQuery.length()) { sbUrl.append("?").append(sbQuery); } } return sbUrl.toString(); } private static HttpClient wrapClient(String host) { HttpClient httpClient = new DefaultHttpClient(); if (host.startsWith("https://")) { sslClient(httpClient); } return httpClient; } private static void sslClient(HttpClient httpClient) { try { SSLContext ctx = SSLContext.getInstance("TLS"); X509TrustManager tm = new X509TrustManager() { public X509Certificate[] getAcceptedIssuers() { return null; } public void checkClientTrusted(X509Certificate[] xcs, String str) { } public void checkServerTrusted(X509Certificate[] xcs, String str) { } }; ctx.init(null, new TrustManager[] { tm }, null); SSLSocketFactory ssf = new SSLSocketFactory(ctx); ssf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); ClientConnectionManager ccm = httpClient.getConnectionManager(); SchemeRegistry registry = ccm.getSchemeRegistry(); registry.register(new Scheme("https", 443, ssf)); } catch (KeyManagementException ex) { throw new RuntimeException(ex); } catch (NoSuchAlgorithmException ex) { throw new RuntimeException(ex); } } }
复制之前短信登录的HTTPUtiles工具类到auth项目
封装一个vo用于接受社交用户的数据
@Data public class SocialUserVo { private String access_token; private String token_type; private long expires_in; private String refresh_token; private String scope; private long created_at;
private long uid; }
member服务的memberController远程接口
@PostMapping("/oauth2/login") public R oauthLogin(@RequestBody SocialUserVo socialUserVo){ MemberEntity memberEntity= null; try { memberEntity = memberService.login(socialUserVo); } catch (Exception e) { log.error("社交登录异常"); } if(memberEntity!=null){ return R.ok().setData(memberEntity); }else{ return R.error(BizCodeEnum.LOGIN_INVALID_EXCEPTION.getCode(),BizCodeEnum.LOGIN_INVALID_EXCEPTION.getMsg()); } }
memberservice的login方法
//具有登录和注册合并逻辑 @Override public MemberEntity login(SocialUserVo socialUserVo) throws Exception { long uid = socialUserVo.getUid(); //判断当前社交用户是否已经登录过系统 MemberEntity user = baseMapper.selectOne(new QueryWrapper<MemberEntity>().eq("social_uid", uid)); if(user!=null){ //这个用户已经注册了 user.setAccessToken(socialUserVo.getAccess_token()); long expires_in = socialUserVo.getExpires_in(); user.setExpiresIn(String.valueOf(expires_in)); baseMapper.updateById(user); return user; }else{ //需要注册一个用户 MemberEntity newMember = new MemberEntity(); //查询当前社交用户的性别,信息,这里我设置没给,就直接设置id就行了 newMember.setAccessToken(socialUserVo.getAccess_token()); newMember.setExpiresIn(String.valueOf(socialUserVo.getExpires_in())); newMember.setSocialUid(String.valueOf(socialUserVo.getUid())); baseMapper.insert(newMember); return newMember; } }
auth调用的远程接口
@FeignClient("gulimall-member") public interface MemberFeignService { @PostMapping("/member/member/regist") R regist(@RequestBody UserRegistVo vo); @PostMapping("/member/member/login") R login(@RequestBody UserLoginVo userLoginVo); @PostMapping("/member/member/oauth2/login") R oauthLogin(@RequestBody SocialUserVo socialUserVo); }
@Slf4j @Controller public class OAuth2Controller { @Autowired MemberFeignService memberFeignService; @GetMapping("/oauth2.0/gitee/success") public String gitee(@RequestParam("code") String code) throws Exception { //根据code换取accessToken HashMap<String, String> map = new HashMap<>(); HashMap<String, String> header = new HashMap<>(); HashMap<String, String> query = new HashMap<>(); map.put("client_id","8351b1529803f1bca29176b023f2c431c48ffe8cd8398165d7bc26baeb6c6f74"); map.put("client_secret","001a283d5c21b12ffd74cbb63c9968e318abbf07611fbb6b2bb1efa60c959fb0"); map.put("grant_type","authorization_code"); map.put("redirect_uri","http://auth.gulimall.com/oauth2.0/gitee/success"); map.put("code",code); HttpResponse response = HttpUtils.doPost("https://gitee.com", "/oauth/token", "post", header, query, map); if(response.getStatusLine().getStatusCode()==200){ //获取到了token //这里得到的就是access_token": "48e3a360bab1a289c882b81a2aa75633", // "token_type": "bearer", // "expires_in": 86400, // "refresh_token": "29cc443c3eb07d366850034475862004ba6b71d590a0dd0cb7ffcb85c82df7fa", // "scope": "user_info", // "created_at": 1629684682---》我猜这个就是uuid // 的json字符串 String string = EntityUtils.toString(response.getEntity()); SocialUserVo socialUserVo = JSON.parseObject(string, SocialUserVo.class); //giee比微博多了一个步骤是需要我们提交一个get请求来获得这个用户的唯一id //https://gitee.com/api/v5/user?access_token=0a7c505a421334e51bc77cad97860bf8 HashMap<String, String> getHeader = new HashMap<>(); HashMap<String, String> getQuery = new HashMap<>(); HttpResponse get = HttpUtils.doGet("https://gitee.com", "/api/v5/user?access_token="+socialUserVo.getAccess_token(), "get", getHeader, getQuery); String s = EntityUtils.toString(get.getEntity()); //拿到的字符串"id":8442725,.... String id = s.split(",")[0].split(":")[1]; long l = Long.parseLong(id); socialUserVo.setUid(l); //当前的用户如果是第一次进网站,那么就需要注册进来(自动注册) //社交用户关联自己系统的会员 R r = memberFeignService.oauthLogin(socialUserVo); if(r.getCode()==0){ MemberRespVo data = r.getData("data", new TypeReference<MemberRespVo>() { }); log.info("登录成功 用户: {}",data.toString()); //远程调用成功 //成功就跳回首页 return "redirect:http://gulimall.com"; }else{ return "redirect:http://auth.gulimall.com/login.html"; } }else{ return "redirect:http://auth.gulimall.com/login.html"; } } }
日志打印
2021-08-23 11:55:10.222 INFO 25316 --- [io-20000-exec-2] c.w.g.auth.controller.OAuth2Controller : 登录成功 用户: MemberRespVo(id=6, levelId=null, username=null, password=null, nickname=null, mobile=null, email=null, header=null, gender=null, birth=null, city=null, job=null, sign=null, sourceType=null, integration=null, growth=null, status=null, createTime=null, socialUid=8442725, accessToken=b3e1d83ba40dc1c6526a55760c1df204, expiresIn=86400)
至此社交登录功能完成
问题存在---首页如何保存用户的登录状态
分布式session
(1) session 原理
session存储在服务端,jsessionId存在客户端,每次通过jsessionid
取出保存的数据
问题:但是正常情况下session
不可跨域,它有自己的作用范围
2) 分布式session解决方案
session要能在不同服务和同服务的集群的共享
1) session复制
用户登录后得到session后,服务把session也复制到别的机器上,显然这种处理很不好
2) hash一致性
根据用户,到指定的机器上登录。但是远程调用还是不好解决
3) redis统一存储
最终的选择方案,把session放到redis中
SpringSession整合
auth和product导入依赖:
<dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
配置修改
spring: #配置nacos
session:
store-type: redis
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
application:
name: gulimall-auth-server
redis:
host: 192.168.116.128
server:
port: 20000
servlet:
session:
timeout: 30m
auth主函数注解,用于存session
后端修改,Vo要想当作数据存到redis里必须实现序列化接口:这个vo需要放在common模块,序列化的问题,不能同时放在product和auth模块
@ToString @Data public class MemberRespVo implements Serializable { private static final long serialVersionUID = 1L; private Long id; private Long levelId; /** * 用户名 */ private String username; /** * 密码 */ private String password; /** * 昵称 */ private String nickname; /** * 手机号码 */ private String mobile; /** * 邮箱 */ private String email; /** * 头像 */ private String header; /** * 性别 */ private Integer gender; /** * 生日 */ private Date birth; /** * 所在城市 */ private String city; /** * 职业 */ private String job; /** * 个性签名 */ private String sign; /** * 用户来源 */ private Integer sourceType; /** * 积分 */ private Integer integration; /** * 成长值 */ private Integer growth; /** * 启用状态 */ private Integer status; /** * 注册时间 */ private Date createTime; /** * 社交账号ID */ private String socialUid; /** * 社交账号Token */ private String accessToken; /** * 社交账号Token过期时间 */ private String expiresIn; }
cookie的domain的问题,作用域不够,所以要提升作用域--需要自己配置
在auth和product模块里,因为是product模块要取,auth模块要存
@Configuration public class RedisSessionConfig { @Bean // redis的json序列化 public RedisSerializer<Object> springSessionDefaultRedisSerializer() { return new GenericJackson2JsonRedisSerializer(); } @Bean // cookie public CookieSerializer cookieSerializer() { DefaultCookieSerializer serializer = new DefaultCookieSerializer(); serializer.setCookieName("GULISESSIONID"); // cookie的键 serializer.setDomainName("gulimall.com"); // 扩大session作用域,也就是cookie的有效域 return serializer; } }
public String gitee(@RequestParam("code") String code, HttpSession session) throws Exception { //根据code换取accessToken HashMap<String, String> map = new HashMap<>(); HashMap<String, String> header = new HashMap<>(); HashMap<String, String> query = new HashMap<>(); map.put("client_id","8351b1529803f1bca29176b023f2c431c48ffe8cd8398165d7bc26baeb6c6f74"); map.put("client_secret","001a283d5c21b12ffd74cbb63c9968e318abbf07611fbb6b2bb1efa60c959fb0"); map.put("grant_type","authorization_code"); map.put("redirect_uri","http://auth.gulimall.com/oauth2.0/gitee/success"); map.put("code",code); HttpResponse response = HttpUtils.doPost("https://gitee.com", "/oauth/token", "post", header, query, map); if(response.getStatusLine().getStatusCode()==200){ String string = EntityUtils.toString(response.getEntity()); SocialUserVo socialUserVo = JSON.parseObject(string, SocialUserVo.class); HashMap<String, String> getHeader = new HashMap<>(); HashMap<String, String> getQuery = new HashMap<>(); HttpResponse get = HttpUtils.doGet("https://gitee.com", "/api/v5/user?access_token="+socialUserVo.getAccess_token(), "get", getHeader, getQuery); String s = EntityUtils.toString(get.getEntity()); String id = s.split(",")[0].split(":")[1]; long l = Long.parseLong(id); socialUserVo.setUid(l); R r = memberFeignService.oauthLogin(socialUserVo); if(r.getCode()==0){ MemberRespVo data = r.getData("data", new TypeReference<MemberRespVo>() { }); log.info("登录成功 用户: {}",data.toString()); //把值传入session中带给前端 session.setAttribute("loginUser",data); //远程调用成功 //成功就跳回首页 return "redirect:http://gulimall.com"; }else{ return "redirect:http://auth.gulimall.com/login.html"; } }else{ return "redirect:http://auth.gulimall.com/login.html"; } }
前端取数据
ps:因为redis缓存的数据product模块要取,auth模块要存,所以vo必须放在公共的模块里,如果仅仅是复制两份会导致包名不一致,序列化拿出不来,config配置也同理,他是一个全局的session配置,不能放在某一模块
SpringSession的原理
最关键的filter
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) { request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository); //对原生的request、response进行包装 // SessionRepositoryRequestWrapper.getSession() SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper( request, response, this.servletContext); SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper( wrappedRequest, response); try { filterChain.doFilter(wrappedRequest, wrappedResponse); } finally { wrappedRequest.commitSession(); } }
后续:
将普通的login也加入到session中,这里不再赘述
当我们的状态是已经登录的时候,进入登录页后我们应该直接跳转到首页,而不是执行登录操作
登录页逻辑
1.删除之前配置的转发登录请求
2.自己的登录逻辑
@GetMapping("/login.html") public String loginPage(HttpSession session){ Object attribute = session.getAttribute(AuthServerConstant.LOGIN_USER); if(attribute==null){ return "login"; }else{ return "redirect:http://gulimall.com"; } }
单点登录
如果旗下有很多产品,我们希望在一个产品的账号在其他产品也能使用
Session不行的原因--》域名放大最大只能放大到一级域名
后续有时间再做