携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情
网页端扫码登录(支付)和手机端跳转微信登录(支付)有空更~
非常非常简单易懂版本!!!
用小区门卫的方式讲懂SpringSecurity
SpringSecurity+jwt认证
jwt是进门凭证
DeniedHandler(403)和EntryPoiny(401)是处理规则
filter就是门卫
SecurityConfig就是将一切组合起来的小区大门!
1. 准备工作
首先我们需要一个AppID和密钥:
在下面官网里可以拿到(当然首先需要注册一个小程序)
2. 后端
我们可以看到jwt的组成部分就是三部分:(jwt就相当于进门的凭证)
头部、载荷和签名
头部是一些信息、载荷是用户信息、签名是判断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>
大致文件结构:
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
第二种是获取用户详细信息,比如微信名之类:
两步我写在一起了:
@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. 前端测试页面
写的非常简单:
文件结构:
页面:
登录的时候会将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文件下)
这里已经测试成功
以上全是个人经验,有错误欢迎评论区批评指正!
后续还会有更多媒介的微信登录包括微信支付模块,喜欢的话求个三连(bushi