上一篇文章写了同步的权限校验,今天发一篇异步请求的
目标
现在做项目大部分都是前后端分离的,不是以前那种垂直的,后端的使用都是调用的接口,比如登录,注册等一些操作,今天的目标就是,用SpringSecurity写一个使用接口登录以及登录成功后返回登录的信息,以及当访问没有权限的接口时,返回json,提示它没权限操作
项目
JDK 1.8
IDEA 2020
Springboot 2.4.0
一、创建新项目
1.使用Spring Initializr创建
填写完项目的包名后,在选依赖时,选中这几个就行了
2.检查下POM有没有缺失
上次因为用Spring Initializr,搞的依赖一直不出来,然后中间一直出错,结果一看pom里面压根没有依赖的坐标,所以养成个习惯。。项目创建好后先看pom.xml
3.配置文件
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.name=defaultDataSource
spring.datasource.url=jdbc:mysql://localhost:3306/liveuser?serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=123456
#Mybatis配置
mybatis.mapper-locations=classpath:/mapper/*.xml
#开启Mybatis下划线命名转驼峰命名
mybatis.configuration.map-underscore-to-camel-case=true
server.port=8080
二、创建数据库
数据库用的还是前面那一篇创建的数据库
/*
Navicat Premium Data Transfer
Source Server : localhost_3306
Source Server Type : MySQL
Source Server Version : 80011
Source Host : localhost:3306
Source Schema : liveuser
Target Server Type : MySQL
Target Server Version : 80011
File Encoding : 65001
Date: 27/11/2020 18:42:16
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for li_role
-- ----------------------------
DROP TABLE IF EXISTS `li_role`;
CREATE TABLE `li_role` (
`id` int(11) NOT NULL,
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for li_user
-- ----------------------------
DROP TABLE IF EXISTS `li_user`;
CREATE TABLE `li_user` (
`id` int(11) NOT NULL,
`user` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`pass` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`role` int(255) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
-- 添加权限
INSERT INTO `liveuser`.`li_role`(`id`, `name`) VALUES (1, 'ROLE_ADMIN')
INSERT INTO `liveuser`.`li_role`(`id`, `name`) VALUES (2, 'ROLE_USER')
-- 添加用户
INSERT INTO `li_user`(`id`, `user`, `pass`, `role`) VALUES (1, 'admin', '12345', 1)
INSERT INTO `li_user`(`id`, `user`, `pass`, `role`) VALUES (2, 'user', '12345', 2)
三、设计功能以及SQL语句
还是只有一个登录
SELECT U.ID,U.USER,U.PASS,R.NAME ROLE FROM LI_USER U LEFT JOIN LI_ROLE R ON R.ID = U.ROLE WHERE U.USER = 'username'
四、设计实体类
没有使用Lombok插件,直接创建的getter/setter方法,那个tostring在测试的时候打印到控制台可以看到里面的属性值,而不是它的内存地址了
package com.example.security2.pojo;
public class User {
Integer id;
String user;
//在转JSON的时候,忽略Pass这个属性
@JsonIgnore
String pass;
String role;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUser() {
return user;
}
public void setUser(String user) {
this.user = user;
}
public String getPass() {
return pass;
}
public void setPass(String pass) {
this.pass = pass;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", user='" + user + '\'' +
", pass='" + pass + '\'' +
", role='" + role + '\'' +
'}';
}
}
五、写Dao层接口
package com.example.security2.dao;
import com.example.security2.pojo.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
@Mapper
@Repository
public interface UserDao {
User login(@Param("username") String username);
}
UserMapper.xml
上一篇直接用的select注解,今天用xml的方式来写,由于把role直接写实体类里面了,这里就不用写resultMap了,直接使用resultType就行了
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.security2.dao.UserDao">
<select id="login" resultType="com.example.security2.pojo.User">
SELECT U.ID,U.USER,U.PASS,R.NAME ROLE FROM LI_USER U LEFT JOIN LI_ROLE R ON R.ID = U.ROLE WHERE U.USER = #{username}
</select>
</mapper>
六、业务
ResultVO
package com.example.security2.vo;
import com.fasterxml.jackson.annotation.JsonInclude;
public class ResultVO<E> {
Integer code;
@JsonInclude(JsonInclude.Include.NON_NULL)
String msg;
@JsonInclude(JsonInclude.Include.NON_NULL)
E data;
public ResultVO(){}
public ResultVO(Integer code,String msg,E data){
this.code = code;
this.msg = msg;
this.data = data;
}
}
UserService
package com.example.security2.service;
import com.example.security2.dao.UserDao;
import com.example.security2.pojo.User;
import com.example.security2.vo.ResultVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Autowired
UserDao userDao;
public ResultVO login(String username, String password){
ResultVO resultVO = new ResultVO();
try{
User user = userDao.login(username);
//迷惑行为,如果密码错了提示用户不存在或密码错误
if(user == null || !user.getPass().equals(password)){
resultVO = new ResultVO(0,"用户不存在或密码错误",null);
}else{
resultVO = new ResultVO(1,"登录成功",user);
}
}catch (Exception ex){
resultVO = new ResultVO(2,"系统错误",ex.getMessage());
}finally {
return resultVO;
}
}
}
七、UserController
package com.example.security2.controller;
import com.example.security2.service.UserService;
import com.example.security2.vo.ResultVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserController {
@Autowired
UserService userService;
@RequestMapping("/login")
public ResultVO Login(String username,String password){
return userService.login(username,password);
}
}
八、测试登录
访问 localhost:8081/login?username=admin&password=12345
因为我们的账户密码是admin和12345,直接跟着get参数访问就行了,最后获取的是我们要拿到的json
从图片上可以看到,json已经打印到我们的浏览器上了,这说明我们的接口一切ok
九、写登录后操作的接口
接下来随便写个接口
@RequestMapping("/test")
public String Test(){
return "test";
}
十、开始配置安全框架
上面是我们不加安全框架的时候的基本操作,接下来我们配置下安全框架
① WebSecurityConfig
package com.example.security2;
import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
@Configurable
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
}
public PasswordEncoder getPasswordEncoder(){
return new PasswordEncoder() {
@Override
public String encode(CharSequence charSequence) {
return charSequence.toString();
}
@Override
public boolean matches(CharSequence charSequence, String s) {
return s.equals(charSequence.toString());
}
};
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/js/**").permitAll()
.anyRequest().authenticated()
.and().formLogin()
.loginProcessingUrl("/check") //异步校验,这个可以随便写路径,到时候使用ajax的时候,让它给这里发请求就行了
.and()
.logout().permitAll();
http.csrf().disable();
}
}
② 创建CustomUserDetailsService
在创建前我们先修改一下UserService,添加一个newLogin的方法,我们用来验证密码正确放到新建的CustomUserDetailsService里面
public User newLogin(String username){
return userDao.login(username);
}
下面就是这个新建的service
package com.example.security2.service;
import com.example.security2.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import sun.text.normalizer.ICUBinary;
import java.util.ArrayList;
import java.util.Collection;
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
UserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userService.newLogin(username);
if(user == null){
throw new UsernameNotFoundException("没有找到这个用户");
}
Collection<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
authorities.add(new SimpleGrantedAuthority(user.getRole()));
return new org.springframework.security.core.userdetails.User(user.getUser(), user.getPass(), authorities);
}
}
这个返回的user(org.springframework.security.core.userdetails.User)只有用户名,密码还有它的权限,如果我们User类中有其他信息的字段,怎么带过去呢??
我们先新建一个NewUserDetail 让他继承UserDetail类
package com.example.security2;
import com.example.security2.vo.ResultVO;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
//让他继承 org.springframework.security.core.userdetails.User
public class NewUserDetails extends User {
com.example.security2.pojo.User userInfo; //创建我们的user实体类属性
public NewUserDetails(com.example.security2.pojo.User user, Collection<? extends GrantedAuthority> authorities) {
super(user.getUser(), user.getPass(), authorities); //这里让他去执行它父类的构造函数
this.userInfo = user; //将传过来的用户实体类赋值给userinfo属性
}
public NewUserDetails(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
}
//新建一个获取ResultVO的类,在这个方法里面创建我们的VO,并把数据添加进去
public ResultVO getResultVO(){
ResultVO resultVO = new ResultVO();
resultVO.setCode(1);
resultVO.setMsg("登录成功");
resultVO.setData(this.userInfo);
return resultVO;
}
}
接下来修改一下我们的CustomUserDetailsService
package com.example.security2.service;
import com.example.security2.NewUserDetails;
import com.example.security2.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import sun.text.normalizer.ICUBinary;
import java.util.ArrayList;
import java.util.Collection;
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
UserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userService.newLogin(username);
if(user == null){
throw new UsernameNotFoundException("没有找到这个用户");
}
Collection<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
authorities.add(new SimpleGrantedAuthority(user.getRole()));
return new NewUserDetails(user,authorities); //将这里的返回创建成我们刚刚新建的一个user
}
}
③ 创建登录成功后的拦截器
为了接收到登录成功的信息,我们先要实现下 AuthenticationSuccessHandler这个接口
package com.example.security2;
import com.fasterxml.jackson.databind.ObjectMapper;
import jdk.nashorn.internal.parser.JSONParser;
import org.apache.ibatis.reflection.wrapper.ObjectWrapper;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Writer;
@Component
public class SuccessHandlerImpl implements AuthenticationSuccessHandler {
ObjectMapper objectMapper;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException {
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
System.out.println("登录成功");
response.setCharacterEncoding("UTF-8"); //设置输出信息的编码格式
NewUserDetails user= (NewUserDetails) authentication.getPrincipal(); //获取登录用户的主体
System.out.println(user.getResultVO()); //这里打印测试下是不是我们要的信息
PrintWriter wirter = response.getWriter();
//下面的objectMapper是jackson中转json的一个对象
if(objectMapper == null){
objectMapper = new ObjectMapper();
}
wirter.println(objectMapper.writeValueAsString(user.getResultVO())); //然他给屏幕上打印我们的json
}
}
④ 创建登录失败后的拦截器
登录失败的时候我们应该也是要返回一段json,创建FailHandleImpl类,让他实现AuthenticationFailureHandler接口
package com.example.security2;
import com.example.security2.vo.ResultVO;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
@Component
public class FailHandleImpl implements AuthenticationFailureHandler {
ObjectMapper objectMapper;
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
System.out.println("登录失败");
response.setCharacterEncoding("UTF-8"); //设置输出信息的编码格式
PrintWriter wirter = response.getWriter();
if(objectMapper == null){
objectMapper = new ObjectMapper();
}
ResultVO resultVO = new ResultVO();
resultVO.setCode(1);
resultVO.setMsg("登录失败,用户名或密码错误");
wirter.println(objectMapper.writeValueAsString(resultVO));
}
}
⑤ 创建WebSecurityConfig类
安全框架的配置类
package com.example.security2;
import com.example.security2.service.CustomUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
@Configurable
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
CustomUserDetailsService customUserDetailsService;
@Autowired
SuccessHandlerImpl successHandler;
@Autowired
FailHandleImpl failHandle;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(customUserDetailsService).passwordEncoder(getPasswordEncoder());
}
public PasswordEncoder getPasswordEncoder(){
return new PasswordEncoder() {
@Override
public String encode(CharSequence charSequence) {
return charSequence.toString();
}
@Override
public boolean matches(CharSequence charSequence, String s) {
return s.equals(charSequence.toString());
}
};
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/js/**").permitAll() //这里用来释放我们的js文件夹内的文件,也就是让jq或者其他js不用登录通行
.anyRequest().authenticated()
.and().formLogin()
.loginProcessingUrl("/check") //异步校验
.successHandler(successHandler) //登录成功的拦截器,使用我们刚刚创建的SuccessHandlerImpl
.failureHandler(failHandle) //登录失败拦截器,FailHandleImpl
.and()
.logout().permitAll();
http.csrf().disable();
}
}
这个时候,我们的登录接口已经完成了,我们测试一下
看下postman测试打印的内容,成功获取到我们登录的信息
这次故意输错用户名
十一、添加权限校验
① 添加权限接口的权限
我们在UserController类中Test方法上加上权限
@RequestMapping("/test")
@PreAuthorize("hasRole('ROLE_ADMIN')")
public String Test(){
return "test";
}
② 测试
我们先请求登录接口登录admin用户,然后在请求test看看是不是输出的test这个字符串
接着postman请求登录接口,登录我们的user用户,然后在用postman请求/test,看下输出什么
403我们有没有办法让他返回JSON,提示权限不足? 往下看
我们创建一个AccessHandleImpl类,让他实现 AccessDeniedHandler接口
package com.example.security2;
import com.example.security2.vo.ResultVO;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
@Component
public class AccessHandleImpl implements AccessDeniedHandler {
ObjectMapper objectMapper;
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
System.out.println("登录失败");
response.setCharacterEncoding("UTF-8"); //设置输出信息的编码格式
PrintWriter wirter = response.getWriter();
if(objectMapper == null){
objectMapper = new ObjectMapper();
}
ResultVO resultVO = new ResultVO();
resultVO.setCode(2);
resultVO.setMsg("权限不足");
wirter.println(objectMapper.writeValueAsString(resultVO));
}
}
修改WebSecurityConfig类
http.authorizeRequests()
.antMatchers("/js/**").permitAll()
.anyRequest().authenticated()
.and().formLogin()
.loginProcessingUrl("/check") //异步校验
.successHandler(successHandler)
.failureHandler(failHandle)
.permitAll()
.and()
.exceptionHandling().accessDeniedHandler(accessHandle) //这里加上我们没有权限时的拦截器
.and()
.logout().permitAll();
http.csrf().disable();
接下来在测试一下
又遇见个问题,如果我们没有登录,然后去直接访问test,则会给我们返回 默认的登录页面,在postman中会看到这个登录页面的html代码,这个怎么解决??
我们在UserController类中加一个方法
@RequestMapping("/logintips")
public ResultVO noLogin(){
return new ResultVO(-1,"未登录",null); //为什么上面没有用这个构造方法直接用的setter,因为。。在前面用构造方法的时候,msg这里一直是null,然后没有找到原因,写到这里的时候发现,,我把msg写成msgm了。。
}
然后在安全框架配置类中添加一个默认的登录页到这个logintips上就好了
.loginPage("/logintips")
然后在来测试一下看看效果
欢迎访问:http://www.fanxing.live