手把手教你学后端微信登录(小程序内登录)

221 阅读6分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情

网页端扫码登录(支付)和手机端跳转微信登录(支付)有空更~

非常非常简单易懂版本!!!

用小区门卫的方式讲懂SpringSecurity

SpringSecurity+jwt认证

jwt是进门凭证

DeniedHandler(403)和EntryPoiny(401)是处理规则

filter就是门卫

SecurityConfig就是将一切组合起来的小区大门!

1. 准备工作

首先我们需要一个AppID和密钥:

在下面官网里可以拿到(当然首先需要注册一个小程序)

mp.weixin.qq.com/wxamp/devpr…

image.png

2. 后端

我们可以看到jwt的组成部分就是三部分:(jwt就相当于进门的凭证)

image.png

头部、载荷和签名

头部是一些信息、载荷是用户信息、签名是判断jwt传输过程中有没有被篡改

想具体了解的可以查看:(更完会把链接挂在这儿)

SpringSecurity框架我这里肯定一篇文章解释不了,想学习可以查看:(更完会把链接挂在这儿)

SpringSecurity相当于一个小区门口

依赖:

<!-- 引入springsecurity -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--引入jwt依赖-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

大致文件结构:

image.png

SecurityContent

保安手里的白名单(无需登录直接放行,有的test接口可以不用登录,登录接口也当然不要登录,要不然就访问不了了):

public class SecurityContents {
    public static final String[] WHITE_LIST = {
            //后端登录接口
            "/user/login",

            //swagger相关
            "/favicon.ico",
            "/swagger-ui.html",
            "/doc.html",
            "/webjars/**",
            "/swagger-resources/**",
            "/v2/**",
            "/configuration/ui",
            "/configuration/security",
            "/tool/forget/password",
            "/tool/sms",
            "/user/sms/login",
            "/goods/batchExport",

            // 小程序相关
            "/mini/login",
            "test",
    };
}

应对的一个场景:权限不足

JwtAccessDeniedHandler:

@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
        response.setStatus(403);
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        PrintWriter writer = response.getWriter();
        writer.write(new ObjectMapper().writeValueAsString(Result.fail("权限不足,请联系管理员!")));
        writer.flush();
        writer.close();
    }
}

另一个拦截场景:用户未登录或者权限不足的时候:

JwtAuthenticationEntryPoint

@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        response.setStatus(401);
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        PrintWriter writer = response.getWriter();
        writer.write(new ObjectMapper().writeValueAsString(Result.fail("您尚未登录,请登录后操作!")));
        writer.flush();
        writer.close();
    }
}

filter

过滤器的作用相当于保安,想进来先看token

1)先看token的Header对不对(就是是不是我这个小程序的token,关于前端怎么传token下面前端会讲) 2)第一步正确之后,再看他的openid,数据库没有登录信息,就新建这个用户,然后刷新SecurityContext里的authorization(这时候就会有靓仔问了,什么时候判断未登录呢?没传token可以看做没有登录,就会被拦截)

@Component
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private TokenUtils tokenUtils;

    @Autowired
    private UserDetailServiceImpl userDetailsService;

    @Value("${jwt.tokenHeader}")
    private String tokenHeader;

    @Value("${jwt.tokenHead}")
    private String tokenHead;

    /**
     * 请求前获取请求头信息token
     * @param request
     * @param response
     * @param chain
     * @throws ServletException
     * @throws IOException
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        log.info("过滤器开始运行...");
        //1. 获取token
        log.info("获取token中");
        String header = request.getHeader(tokenHeader);
        log.info("header:"+ header);
        //2. 判断token是否存在
        if (null != header && header.startsWith(tokenHead)) {
            log.info("token存在");
            log.info("解析token");
            //拿到token主体
            String token = header.substring(tokenHead.length());
            log.info("token主体:"+ token);
            //根据token获取openid
            String openid = tokenUtils.getOpenidByToken(token);
            log.info("从token中获取openid:"+ openid);
            //3. token存在,但是没有登录信息
            if (null != openid && null == SecurityContextHolder.getContext().getAuthentication()) {
                //没有登录信息,直接登录
                UserDetails userDetails = userDetailsService.loadUserByUsername(openid);
                //判断token是否有效
                if (!tokenUtils.isExpiration(token) && openid.equals(userDetails.getUsername())) {
                    log.info("jwt的openid和数据库中的openid一致");
                    //刷新security中的用户信息
                    log.info("刷新security中的用户信息");
                    UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                    authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                }
            }
        }
        //过滤器放行
        log.info("过滤器放行...");
        chain.doFilter(request, response);
    }
}

UserDetailsService的重写

这里会涉及一个UserDetailsService的重写,相当于保安看到新访客之后让他填写表进去(登记新访客,给他发个凭证)这个部分自定义

@Slf4j
@Service
public class UserDetailServiceImpl implements UserDetailsService {

    @Resource
    private SysUserMapper userMapper;

    @Autowired
    private RedisUtils redisUtil;

    @Override
    public UserDetails loadUserByUsername(String username) {
        //判断缓存中是否存在用户信息 存在则直接从缓存中取,不存在则查询数据库并把数据存入缓存
        SysUser user;
        if (redisUtil.haskey("userInfo_" + username)) {
            //缓存中存在用户信息,直接从redis中取
            user = (SysUser) redisUtil.getValue("userInfo_" + username);
            redisUtil.expire("userInfo_" + username, 5);
            log.info("成功在Redis中找到用户" + username);
        } else {
            user = userMapper.findByUsername(username);
//            if (null == user) {
////                throw new UsernameNotFoundException("用户名或密码错误!");
//                System.out.println(username + "是新用户!");
//            }
            if (user != null) {

                if (user.isAdmin()) {
                    //非管理员需要查询角色信息
                    user.setRoles(userMapper.findRoles(null));
                    user.setPermissions(userMapper.findPermissions(null));

                } else {
                    //非管理员需要查询角色信息
                    user.setRoles(userMapper.findRoles(user.getId()));
                    user.setPermissions(userMapper.findPermissions(user.getId()));

                }
                redisUtil.setValueTime("userInfo_" + username, user, 5);
            }

        }
        return user;
    }
}

SecurityConfig

接下来是核心配置:

相当于有了保安,有个规则,就要把保安部署起来:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailServiceImpl userDetailService;

    @Autowired
    private JwtAuthenticationEntryPoint authenticationEntryPoint;

    @Autowired
    private JwtAccessDeniedHandler accessDeniedHandler;

    @Autowired
    private JwtAuthenticationFilter authenticationFilter;

    /**
     * 一般用于配置白名单
     * 白名单:可以没有权限也可以访问的资源
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring()
            .mvcMatchers(SecurityContents.WHITE_LIST);
    }

    /**
     * Security的核心配置
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //1. 使用jwt,首先关闭跨域攻击
        http.csrf().disable();
        //2. 关闭session
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        //3. 请求都需要进行认证之后才能访问,除白名单以外的资源
        http.authorizeRequests().anyRequest().authenticated();
        //4. 关闭缓存
        http.headers().cacheControl();
        //5. token过滤器,校验token
        http.addFilterBefore(authenticationFilter, UsernamePasswordAuthenticationFilter.class);
        //6. 没有登录、没有权限访问资源自定义返回结果
        http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).accessDeniedHandler(accessDeniedHandler);
    }

    /**
     * 自定义登录逻辑的配置
     * 也即是配置到security中进行认证
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailService).passwordEncoder(passwordEncoder());
    }

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

}

ok!大门部署起来了

登录

接下来就是登录接口怎么写了:

这里分两种:

一种是用户点进这个小程序就会自动触发一个登录,这个写在前端的onLaunch里,就是登录就会触发,但是也只会登记一个openid

第二种是获取用户详细信息,比如微信名之类:

image.png

两步我写在一起了:

@Slf4j
@RestController
@RequestMapping("/mini")
@Api(tags = "小程序相关接口")
public class MiniController {

    @Value("${mini.appid}")
    private String appid;

    @Value("${mini.secret}")
    private String secret;

    @Autowired
    private SysUserService userService;


    @ApiOperation(value = "微信小程序登录")
    @GetMapping("/login")
    public Result login(String code) {
        log.info("-----------------------------------------------------------------------------------------------------------------------------------");
        log.info("收到微信小程序登录请求");
        log.info("code:" + code);
        if (StringUtils.isEmpty(code)) {
            log.info("code为空");
            return Result.fail("code为空!");
        }
        // 构建get请求
        String url = "https://api.weixin.qq.com/sns/jscode2session?" + "appid=" +
                appid +
                "&secret=" +
                secret +
                "&js_code=" +
                code +
                "&grant_type=authorization_code";
        String result = HttpUtils.getResponse(url);
        // 发送请求
        log.info("发送code给官网");
        JSONObject jsonObject = JSON.parseObject(result);
        String openid = jsonObject.getString("openid");
        String sessionKey = jsonObject.getString("session_key");
        log.info("code中的openid:" + openid);
        log.info("code中的session_key:" + sessionKey);
        return userService.miniLogin(openid, sessionKey);
    }

    @ApiOperation(value = "更新用户信息")
    @PostMapping("/update/info")
    public Result updateInfo(@RequestBody SysUser user) {
        log.info("------------------------------------------------------------------------------------------------------------------------------------------------------------");
        log.info("收到更新用户信息的请求");
        return userService.updateByOpenId(user);
    }



}

涉及到的service层接口:

@Service
@Slf4j
public class SysUserServiceImpl implements SysUserService {

    @Resource
    private SysUserMapper userMapper;

    @Autowired
    private TokenUtils tokenUtils;

    @Autowired
    private UserDetailServiceImpl userDetailsService;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Value("${jwt.tokenHead}")
    private String tokenHead;

    @Autowired
    private RedisUtils redisUtils;

    /**
     * 微信小程序登录
     * @param openid 登录参数: 账号和密码
     * @return
     */
    @Override
    public Result miniLogin(String openid, String sessionKey) {
        log.info("开始小程序登录...");
        UserDetails userDetails;
        // sc: 这里虽然叫loadByUsername,但是可以根据电话、username、openid获取用户
        userDetails = userDetailsService.loadUserByUsername(openid);
        log.info("userDetailsService根据openid查找用户中...");
        if (userDetails == null) {
            log.info("没有查询到,是第一次登陆");
            userMapper.insertOpenid(openid);
            log.info("数据库插入新用户");
            userDetails = userDetailsService.loadUserByUsername(openid);
        }
        if (!userDetails.isEnabled()) {
            log.info("该账号未启用,请联系管理员!");
            return Result.fail("该账号未启用,请联系管理员!");
        }
        log.info("微信小程序登录成功,在security对象中存入登陆者信息");
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        log.info("根据登录信息获取token");
        //需要借助jwt来生成token
        String token = tokenUtils.generateToken(userDetails);
        Map<String, Object> map = new HashMap<>(5);
        map.put("tokenHead", tokenHead);
        map.put("token", token);
        map.put("userInfo", userDetails);
        map.put("openid", openid);
        map.put("sessionKey", sessionKey);
        log.info("登录成功!");
        log.info("返回jwt信息");
        log.info("------------------------------------------------------------------------------------------------------------------------------------------------------------");
        return Result.success("登录成功!", map);
    }

    @Override
    public Result updateByOpenId(SysUser user) {
        if (StringUtils.isEmpty(user.getOpenId())) {
            log.info("openid为空");
            log.info("----------------------------------------------------------------------------------------------------------------------------------");
            return Result.fail("openid为空");
        }
        // 清除用户缓存,重新获取
        redisUtils.delKey("username_" + user.getOpenId());
        log.info("清除Redis缓存");
        userMapper.updateByopenId(user);
        log.info("更新用户信息成功");
        log.info("--------------------------------------------------------------------------------------------------------------------------------------------------------");
        return Result.success("用户信息更新成功", user);
    }
}

ok,后端完活

4. 前端测试页面

写的非常简单:

文件结构:

image.png

页面:

image.png

登录的时候会将token存在storage中,每次发请求的时候带上请求头即可

代码直接全选+cv即可:

app.js:

// app.js
App({
  onLaunch() {
      // 登录
      wx.login({
          success(res) {  
              console.log('请求成功!');    
              if (res.code) {      
                  //发起网络请求
                  wx.request({      
                      url: 'http://localhost:8080/mini/login',       
                      data: {      
                          code: res.code       
                      },     
                      success: (res) => {      
                          console.log("登录成功!");   
                          console.log('Bearer '+res.data.data.token); 
                          const {       
                              flag,     
                              data,      
                              message      
                          } = res.data;   
                          if (!flag) {      
                              return wx.showToast({      
                                  title: message, 
                                  icon: 'error', 
                                  duration: 2000  
                              }); 
                          }  

                       wx.setStorageSync('totken', `${data.tokenHead} ${data.token}`);
                          
                          wx.setStorageSync('userInfo', data.userInfo);
                          wx.setStorageSync('openid', data.openid); 
                      },          
                      fail: (err) => {   
                          console.log('接口请求失败: -->', err);              
                      }
                  });  
              } else {
                  console.log('登录失败!' + res.errMsg)
              }
          }
      });   
  },  
});   

index.js:

// index.js
// 获取应用实例
const app = getApp()

Page({
  data: {
    userInfo: {},
  },
  getInfo() { 
    wx.getUserProfile({
      desc: '用于完善用户资料',
      success: (res) => {    
        let { avatarUrl, nickName, gender, country, province, city } = res.userInfo;
        wx.request({
          url: 'http://localhost:8080/mini/update/info',
        
          method: 'POST',
          header: {
            'Authorization': wx.getStorageSync('totken')
          }, 
          data: {
            nickName: nickName,
            sex: gender, 
            avatar: avatarUrl,
            address: `${country} ${province} ${city}`,    
            // `${data.tokenHead} ${data.token}`
            openId: wx.getStorageSync('openid')
          },
          success: (res) => {
            console.log(res);
          },
          fail: (err) => {
            console.log(err); 
          }      
        });   
      }
    })
  },
 test() {
  wx.request({
    url: 'http://localhost:8080/test',
    method: 'GET',
    header: {
      'Authorization': wx.getStorageSync('totken')
    },
    data: {

    } ,
    success: (res) => {
      console.log(res);
    },
    fail: (err) => {
      console.log(err); 
    }
  })
 },
 getCvByOpenId() {
  wx.request({

    url: 'http://localhost:8080/api/cv/getCvByOpenId',
    method: 'GET',
    header: {
      'Authorization': wx.getStorageSync('totken')
    },
    data: {
      'openId': wx.getStorageSync('openid')
    } ,
    success: (res) => {
      console.log(res);
    },
    fail: (err) => {
      console.log(err); 
    }
  })
 }
})


index.wxml:

<!--index.wxml-->
<view class="container">
  <view class="userinfo">
    <button type="primary" bindtap="getInfo">获取信息</button>
  </view>
  <view class="test">
    <button type="primary" bindtap="test">test</button>
  </view>
  <view class="getCvByOpenId">
    <button type="primary" bindtap="getCvByOpenId">getCvByOpenId</button>
  </view>
</view> 

前端完活

5. 测试

先启动后端:

tips

因为是onLanuch里的登录方法,所以只要在空白处加个空格再ctrl+s保存就会自动登录(注意要在app.js文件下)

这里已经测试成功

image.png

以上全是个人经验,有错误欢迎评论区批评指正!

后续还会有更多媒介的微信登录包括微信支付模块,喜欢的话求个三连(bushi