业务流程图:
一、页面拦截验证
1.1、初始化Passport-web的springboot应用
1.2、在web-util中配置拦截器
@Configuration
public class WebMvcConfiguration extends WebMvcConfigurerAdapter {
@Autowired
AuthInterceptor authInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry){
registry.addInterceptor(authInterceptor).addPathPatterns("/**");
super.addInterceptors(registry);
}
}
定义LoginRequired注解,当拦截器拦击方法标注有该注解时需要登录,当注解的loginSuccess属性值为true时必须登录用户。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginRequired
{
boolean loginSuccess() default true;
}
@Component
public class AuthInterceptor extends HandlerInterceptorAdapter {
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 拦截代码
//判断被拦截的请求的访问的方法的注解(是否是需要拦截的)
HandlerMethod hm = (HandlerMethod)handler;
LoginRequired methodAnnotation = hm.getMethodAnnotation(LoginRequired.class);
//是否拦截
if(methodAnnotation==null){
return true;
}
/**
* 如果oldToken为null newToken为null 说明从未登lu过
* newToken为null oldToken不为null之前登录过
* newToken不为null oldToken为null刚刚登录过
* newToken不为空 oldToken不为空 oldToken过期
*/
String token = "";
String oldToken = CookieUtil.getCookieValue(request,"oldToken",true);
if(StringUtils.isNotBlank(oldToken)){
token = oldToken;
}
String newToken = request.getParameter("token");
if(StringUtils.isNotBlank(newToken)){
token = newToken;
}
//是否必须登录
boolean loginSuccess = methodAnnotation.loginSuccess();
//调用认证中心进行验证
String success = "fail";
String ip = request.getHeader("x-forwarded-for");//通过nginx转发的获得的客户端ip
if(StringUtils.isBlank(ip)){
ip = request.getRemoteAddr();//从request中会的ip
if(StringUtils.isBlank(ip)){
ip = "192.168.157.1";
}
}
String successJSON = HttpclientUtil.doGet("http://localhost:8085/verify?token="+token+"¤tIp="+ip);
Map successMap = JSON.parseObject(successJSON, Map.class);
if(successMap.get("status")!=null){
success = successMap.get("status").toString();
}
if(loginSuccess==true){
if(!success.equals("success")){
//重定向到passport登录
response.sendRedirect("http://localhost:8085/index?ReturnUrl="+request.getRequestURL());
return false;
}else{
//验证通过,覆盖cookie中的tocken
//需要将tockn携带的用户信息写入
request.setAttribute("memberId",successMap.get("memberId"));
request.setAttribute("nickname",successMap.get("nickname"));
//验证通过,覆盖cookie中的token
if(StringUtils.isNotBlank(token)){
CookieUtil.setCookie(request,response,"oldToken",token,60*60*2,true);
}
return true;
}
}else{
//没有登良成功也能够使用功能,但是必须验证
if(success.equals("success")){
//需要将tockn携带的用户信息写入
request.setAttribute("memberId",successMap.get("memberId"));
request.setAttribute("nickname",successMap.get("nickname"));
//验证通过,覆盖cookie中的token
if(StringUtils.isNotBlank(token)){
CookieUtil.setCookie(request,response,"oldToken",token,60*60*2,true);
}
}
}
return true;
}
}
二、登录验证(生成token)
passport只负责认证和token的颁发
1、 用接受的用户名密码核对后台数据库
2、 将用户信息加载到写入redis,redis中有该用户视为登录状态。
3、 用userId+当前用户登录ip地址+密钥生成token
4、 重定向用户到之前的来源地址(index页面保存的ReturnUrl),同时把token作为参数附上。
2.1、核对后台登录信息+用户登录信息载入缓存
2.2、生成token
JWT(Json Web Token) 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准。
JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源。比如用在用户登录上
JWT 最重要的作用就是对 token信息的防伪作用。
1、 公共部分
主要是该JWT的相关配置参数,比如签名的加密算法、格式类型、过期时间等等。
2、 私有部分
用户自定义的内容,根据实际需要真正要封装的信息。
3、 签名部分
根据用户信息+盐值+密钥生成的签名。如果想知道JWT是否是真实的只要把JWT的信息取出来,加上盐值和服务器中的密钥就可以验证真伪。所以不管由谁保存JWT,只要没有密钥就无法伪造。
4、 base64编码,并不是加密,只是把明文信息变成了不可见的字符串。但是其实只要用一些工具就可以吧base64编码解成明文,所以不要在JWT中放入涉及私密的信息,因为实际上JWT并不是加密信息。
导入相关依赖:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
制作 JWT的工具类(JwtUtil)
public class JwtUtil {
public static String encode(String key,Map<String,Object> param,String salt){
if(salt!=null){
key+=salt;
}
JwtBuilder jwtBuilder = Jwts.builder().signWith(SignatureAlgorithm.HS256,key);
jwtBuilder = jwtBuilder.setClaims(param);
String token = jwtBuilder.compact();
return token;
}
public static Map<String,Object> decode(String token ,String key,String salt){
Claims claims=null;
if (salt!=null){
key+=salt;
}
try {
claims= Jwts.parser().setSigningKey(key).parseClaimsJws(token).getBody();
} catch ( JwtException e) {
return null;
}
return claims;
}
}
Cookie工具类
public class CookieUtil
{
/***
* 获得cookie中的值,默认为主ip:www.com.xatu.gmall.com
* @param request
* @param cookieName
* @param isDecoder
* @return
*/
public static String getCookieValue(HttpServletRequest request, String cookieName, boolean isDecoder) {
Cookie[] cookies = request.getCookies();
if (cookies == null || cookieName == null){
return null;
}
String retValue = null;
try {
for (int i = 0; i < cookies.length; i++) {
if (cookies[i].getName().equals(cookieName)) {
if (isDecoder) {//如果涉及中文
retValue = URLDecoder.decode(cookies[i].getValue(), "UTF-8");
} else {
retValue = cookies[i].getValue();
}
break;
}
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return retValue;
}
/***
* 设置cookie的值
* @param request
* @param response
* @param cookieName
* @param cookieValue
* @param cookieMaxage
* @param isEncode
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, int cookieMaxage, boolean isEncode) {
try {
if (cookieValue == null) {
cookieValue = "";
} else if (isEncode) {
cookieValue = URLEncoder.encode(cookieValue, "utf-8");
}
Cookie cookie = new Cookie(cookieName, cookieValue);
if (cookieMaxage >= 0)
cookie.setMaxAge(cookieMaxage);
if (null != request)// 设置域名的cookie
cookie.setDomain(getDomainName(request));
// 在域名的根路径下保存
cookie.setPath("/");
response.addCookie(cookie);
} catch (Exception e) {
e.printStackTrace();
}
}
/***
* 获得cookie的主域名,本系统为gmall.com,保存时使用
* @param request
* @return
*/
private static final String getDomainName(HttpServletRequest request) {
String domainName = null;
String serverName = request.getRequestURL().toString();
if (serverName == null || serverName.equals("")) {
domainName = "";
} else {
serverName = serverName.toLowerCase();
serverName = serverName.substring(7);
final int end = serverName.indexOf("/");
serverName = serverName.substring(0, end);
final String[] domains = serverName.split("\\.");
int len = domains.length;
if (len > 3) {
// www.xxx.com.cn
domainName = domains[len - 3] + "." + domains[len - 2] + "." + domains[len - 1];
} else if (len <= 3 && len > 1) {
// xxx.com or xxx.cn
domainName = domains[len - 2] + "." + domains[len - 1];
} else {
domainName = serverName;
}
}
if (domainName != null && domainName.indexOf(":") > 0) {
String[] ary = domainName.split("\\:");
domainName = ary[0];
}
System.out.println("domainName = " + domainName);
return domainName;
}
/***
* 将cookie中的内容按照key删除
* @param request
* @param response
* @param cookieName
*/
public static void deleteCookie(HttpServletRequest request, HttpServletResponse response, String cookieName) {
setCookie(request, response, cookieName, null, 0, false);
}
}
passport模块负责登陆验证、token的生成和token真伪的验证
@Controller
public class PassportController {
@Reference
UserService userService;
@RequestMapping("/index")
public String index(String ReturnUrl, ModelMap map){
map.put("ReturnUrl", ReturnUrl);
return "index";
}
@ResponseBody
@RequestMapping("/login")
public String login(Member loginMember, HttpServletRequest request){
String token = "";
//调用用户服务,验证用户名和密码
Member member = userService.login(loginMember);
if(member!=null){
//登陆成功
//用jwt制作token
Long memberId = member.getId();
String nickName = member.getNickname();
Map<String,Object> userMap = new HashMap<>();
userMap.put("memberId",memberId);
userMap.put("nickName",nickName);
String ip = request.getHeader("x-forwarded-for");//通过nginx转发的获得的客户端ip
if(StringUtils.isBlank(ip)){
ip = request.getRemoteAddr();//从request中会的ip
if(StringUtils.isBlank(ip)){
ip = "192.168.157.1";
}
}
//按照设计的算法对参数进行加密,生成token
token = JwtUtil.encode("2020gmall",userMap,ip);
//将token存入redis一份
userService.addToken(token,memberId);
}else{
//登陆失败
token = "fail";
}
return token;
}
@RequestMapping("/verify")
@ResponseBody
public String verify(String token,String currentIp){
//通过jwt校验token真假
Map<String,String> map = new HashMap<>();
Map<String, Object> decode = null;
if(StringUtils.isNotBlank(token)&&StringUtils.isNotBlank(currentIp)){
decode = JwtUtil.decode(token, "2020gmall", currentIp);
}
if(decode!=null){
map.put("status","success");
map.put("memberId",decode.get("memberId").toString());
map.put("nickName",decode.get("nickName").toString());
}else{
map.put("status","fail");
}
return JSON.toJSONString(map);
}
}
Service层登陆验证
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, Member> implements UserService {
@Autowired
UserMapper userMapper;
@Autowired
MemberReceiveAddressMapper memberReceiveAddressMapper;
@Autowired
RedisUtil redisUtil;
public List<Member> selectUserById(Integer i) {
return userMapper.SelectUserById(1);
}
public List<MemberReceiveAddress> getReceiveAddressByMemberId(String memberId) {
return (List<MemberReceiveAddress>) memberReceiveAddressMapper.selectList(new QueryWrapper<MemberReceiveAddress>().eq("member_Id",memberId));
}
@Override
public Member login(Member loginMember) {
Jedis jedis = null;
Member memberFromCache = null;
Member memberFromDB = null;
try{
jedis = redisUtil.getJedis();
if (jedis!=null){
String memberStr = jedis.get("user:" + loginMember.getUsername()+loginMember.getPassword()+ ":info");
if(memberStr!=null){
//密码正确
memberFromCache = JSON.parseObject(memberStr, Member.class);
return memberFromCache;
}else{
//密码错误
//缓存中没有开启数据库
memberFromDB = loginFromDB(loginMember);
if(memberFromDB!=null){
jedis.setex("user:" + memberFromDB.getUsername()+memberFromDB.getPassword()+ ":info",60*60*24,JSON.toJSONString(memberFromDB));
}
return memberFromDB;
}
}else{
//缓存宕机失败
//查询数据库(分布式锁)
memberFromDB = loginFromDB(loginMember);
if(memberFromDB!=null){
jedis.setex("user:" + memberFromDB.getUsername()+memberFromDB.getPassword()+ ":info",60*60*24,JSON.toJSONString(memberFromDB));
}
return memberFromDB;
}
}finally {
jedis.close();
}
}
@Override
public void addToken(String token, Long memberId) {
Jedis jedis = redisUtil.getJedis();
jedis.setex("user:"+memberId+":token",60*60*2,token);
jedis.close();
}
private Member loginFromDB(Member loginMember) {
return userMapper.selectOne(new QueryWrapper<Member>().eq("username",loginMember.getUsername()).eq("password",loginMember.getPassword()));
}
}
三、登录页面请求跳转
拦截器拦截请求,当页面请求过来首先验证是否需要登录才能使用功能。在隐藏域中保存发送的页面请求地址为ReturnUrl,当服务器拦截器验证成功后以备页面进行跳转。
在登陆页面设置隐藏域
<!--底部-->
<input type="text" th:value="${ReturnUrl}" id="ReturnUrl" />
拦截器拦截到请求后重定向到登陆页面,在请求中拼接ReturnUrl
//重定向到passport登录
response.sendRedirect("http://localhost:8085/index?ReturnUrl="+request.getRequestURL());
当用户登陆成功后从登陆页面获取隐藏域中的ReturnUrl跳转到对应地址
function submitLogin() {
var username = $("#username").val();
var password = $("#password").val();
$.post("login",{username:username,password:password},function(token){
// 验证token是否为空或者异常
if(token == "fail"){
alert("用户名或密码错误!");
}else{
window.location.href=$("#ReturnUrl").val()+"?token="+token;
}
});
}