超全的springboot+springsecurity实现前后端分离简单实现!
1、前言部分
1.1、唠嗑部分(如何学习?)
看springsecurtiy原理图的时候以为洒洒水,妈的,结果自己动手做的时候一窍不通,所以一定不要眼高手低,实践出真知! 通过各种方式学习springsecurity,在B站、腾讯课堂、网易课堂、慕课网没有springsecurity的前后端分离的教学视频,那我就去csdn去寻找springsecurity博客,发现几个问题:
-
要么就是前后端不分离,要么就是通过内存方式读取数据,而不是通过数据库的方式读取数据,要么就是大佬们给的代码不全、把代码讲的太绕,关键部分没有注释。
-
讲的例子不那么通俗易懂,不利于新手的学习。
-
代码本身有bug,或者就没有我想要实现的效果。
实在不行我又跑去github上找开源项目学习,github由于是外国网站,国内访问速度有点慢!!那就用国内的gitee吧,gitee上的开源项目都是结合实战项目的,代码逻辑也比较复杂,我对项目的业务逻辑没什么了解,感觉不适合我。我这一次选择比较反人性的方式去学习,就是手撕源码和看官方文档。老实讲,刚开始看源码和官方文档特别难受,并且看不进去,那些springsecurity的类还有接口名字又臭又长,这时我就下载源码,源码的注释多的就像一本书,非常详细且权威。
当然别指望看几遍就能看懂,我看这些注释、源码、博客看了10几遍甚至20几遍才看懂,每次去看都有不同的收获!!!
此文章截图水平不高、理解为主、欣赏为辅!!内容有点多,每一步都有详细解析,请耐心看完,看不懂可以多看几遍。。
1.2、技术支持
jdk 1.8、springboot 2.3.4、mybatis-plus 3.4.1、mysql 5.5、springsecurity 5.3.4、springmvc、lombok简化entity代码,不用你去写get、set方法,全部自动生成、gson 2.8.2 将json对象转化成json字符串
1.3、预期实现效果图
未登录时访问指定资源, 返回未登录的json字符串 , index是我在controller层写的一个简单接口,返回index字符串
输入账号错误,返回用户名错误的json字符串 , 需说明一点,/login是springsecurity封装好的接口,无须你在controller写login接口,/logout也同理。
输入密码错误,返回密码错误的json字符串
登录成功, 返回登录成功的json字符串并返回cookie
登录成功并且拥有权限访问指定资源, 返回资源相关数据的json字符串
登录成功但无权限访问指定资源时,返回权限不足的json字符串
异地登录,返回异地登录,强制下线的json字符串 , 测试的基础是要在两台不同的机器上登录,然后访问/index。
注销成功,返回注销成功的json字符串并删除cookie
2、核心部分
2.1、springsecurity原理解释:
springsecurity最重要的两个部分: authentication(认证) 和 authorization(授权)
认证: 就是判定你是什么身份,管理员还是普通人
授权: 什么样的身份拥有什么样的权利。
简单理解: 自定义配置登录成功、登陆失败、注销成功目标结果类,并将其注入到springsecurity的配置文件中。如何认证、授权交给AuthenticationManager去作
复杂理解:
(1)用户发起表单登录请求后,首先进入 UsernamePasswordAuthenticationFilter
, 在 UsernamePasswordAuthenticationFilter 中根据用户输入的用户名、密码构建了 UsernamePasswordAuthenticationToken
,并将其交给 AuthenticationManager 来进行认证处理。
AuthenticationManager 本身不包含认证逻辑,其核心是用来管理所有的 AuthenticationProvider
,通过交由合适的 AuthenticationProvider 来实现认证。
(2)下面跳转到了 SelfAuthenticationProvider
,该类是 AuthenticationProvider 的实现类:你可以在该类的 Authentication authenticate(Authentication authentication)
自定义认证逻辑, 然后在该类中通过调用UserDetails loadUserByUsername(account)
去获取数据库用户信息并验证,然后创建 UsernamePasswordAuthenticationToken
并将权限、用户个人信息注入到其中 ,并通过setAuthenticated(true)
设置为需要验证。
(3) 至此认证信息就被传递回 UsernamePasswordAuthenticationFilter 中,在 UsernamePasswordAuthenticationFilter 的父类 AbstractAuthenticationProcessingFilter
的 doFilter()
中,会根据认证的成功或者失败调用相应的 handler: 所谓的handler就是我们注入到springsecurity配置文件的handler。
2.2、踩坑集锦
- 访问/login时必须要用post方法!, 访问的参数名必须为username和password
- 访问/logout时即可用post也可用get方法!
- //springsecurity配置文件中的hasRole("")不能以ROLE开头,比如ROLE_USER就是错的,springsecurity会默认帮我们加上,但数据库的权限字段必须是ROLE_开头,否则读取不到
.antMatchers("/index").hasRole("USER")
.antMatchers("/hello").hasRole("ADMIN")
2.3、代码部分
pom依赖文件
<dependencies>
<!--转换成json字符串的工具-->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.2</version>
</dependency>
<!--springboot集成web操作7-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--springsecurity-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--lombok依赖-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--mybatis-plus依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
<!--springboot-自带测试工具-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>5.3.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>5.3.4.RELEASE</version>
</dependency>
</dependencies>
Msg.java 自定义返回结果集,这个看个人的,怎么开心怎么来!
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Msg {
int code; //错误码
String Message; //消息提示
Map<String,Object> data=new HashMap<String,Object>(); //数据
//无权访问
public static Msg denyAccess(String message){
Msg result=new Msg();
result.setCode(300);
result.setMessage(message);
return result;
}
//操作成功
public static Msg success(String message){
Msg result=new Msg();
result.setCode(200);
result.setMessage(message);
return result;
}
//客户端操作失败
public static Msg fail(String message){
Msg result=new Msg();
result.setCode(400);
result.setMessage(message);
return result;
}
public Msg add(String key,Object value){
this.data.put(key,value);
return this;
}
}
User.java ,此类是entity实体类
@Data
public class User implements Serializable {
private Integer id;
private String account;
private String password;
private String role;
}
UserMapper.java ,此接口继承 BaseMapper<T>类,而BaseMapper<T>类 封装了大量的sql,极大程度简化了程序员sql语句的书写.
@Repository
public interface UserMapper extends BaseMapper<User> {
}
正常情况下要写UserService.java接口,但是此文章只是用于演示效果,就没书写了
public interface UserService{
}
UserServiceImpl.java,使其实现 UserDetailsService接口, 从而去获取数据库用户信息,详细解析请看注释部分。
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService,UserDetailsService {
@Autowired
UserMapper userMapper;
//加载用户
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
//mybatis-plus帮我们写好了sql语句,相当于 select * from user where account ='${account}'
QueryWrapper<User> wrapper=new QueryWrapper<>();
wrapper.eq("account",s);
User user=userMapper.selectOne(wrapper); //user即为查询结果
if(user==null){
throw new UsernameNotFoundException("用户名错误!!");
}
//获取用户权限,并把其添加到GrantedAuthority中
List<GrantedAuthority> grantedAuthorities=new ArrayList<>();
GrantedAuthority grantedAuthority=new SimpleGrantedAuthority(user.getRole());
grantedAuthorities.add(grantedAuthority);
//方法的返回值要求返回UserDetails这个数据类型, UserDetails是接口,找它的实现类就好了
//new org.springframework.security.core.userdetails.User(String username,String password,Collection<? extends GrantedAuthority> authorities) 就是它的实现类
return new org.springframework.security.core.userdetails.User(s,user.getPassword(),grantedAuthorities);
}
}
UserController.java 就是普通的controller
@RestController
public class UserController {
@GetMapping("index")
public String index(){
return "index";
}
@GetMapping("hello")
public String hello(){
return "hello";
}
}
AuthenticationEnryPoint .java 自定义未登录的处理逻辑
@Component
public class AuthenticationEnryPoint implements AuthenticationEntryPoint {
@Autowired
Gson gson;
//未登录时返回给前端数据
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
Msg result=Msg.fail("需要登录!!");
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(gson.toJson(result));
}
}
AuthenticationFailure.java 自定义登录失败时的处理逻辑
//登录失败返回给前端消息
@Component
public class AuthenticationFailure implements AuthenticationFailureHandler{
@Autowired
Gson gson;
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
Msg msg=null;
if(e instanceof UsernameNotFoundException){
msg=Msg.fail(e.getMessage());
}else if(e instanceof BadCredentialsException){
msg=Msg.fail("密码错误!!");
}else {
msg=Msg.fail(e.getMessage());
}
//处理编码方式,防止中文乱码的情况
response.setContentType("text/json;charset=utf-8");
//返回给前台
response.getWriter().write(gson.toJson(msg));
}
}
AuthenticationSuccess.java 自定义登录成功时的处理逻辑
@Component
public class AuthenticationSuccess implements AuthenticationSuccessHandler{
@Autowired
Gson gson;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//登录成功时返回给前端的数据
Msg result=Msg.success("登录成功!!!!!");
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(gson.toJson(result));
}
}
AuthenticationLogout.java 自定义注销时的处理逻辑
@Component
public class AuthenticationLogout implements LogoutSuccessHandler{
@Autowired
Gson gson;
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
Msg result=Msg.success("注销成功");
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(gson.toJson(result));
}
}
AccessDeny.java 自定义无权限访问时的逻辑处理
//无权访问
@Component
public class AccessDeny implements AccessDeniedHandler{
@Autowired
Gson gson;
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
Msg result= Msg.denyAccess("无权访问,need Authorities!!");
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(gson.toJson(result));
}
}
SessionInformationExpiredStrategy.java 自定义异地登录、账号下线时的逻辑处理
@Component
public class SessionInformationExpiredStrategy implements org.springframework.security.web.session.SessionInformationExpiredStrategy{
@Autowired
Gson gson;
@Override
public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
Msg result= Msg.fail("您的账号在异地登录,建议修改密码");
HttpServletResponse response=event.getResponse();
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(gson.toJson(result));
}
}
SelfAuthenticationProvider.java 自定义认证逻辑处理
@Component
public class SelfAuthenticationProvider implements AuthenticationProvider{
@Autowired
UserServiceImpl userServiceImpl;
@Autowired
BCryptPasswordEncoder bCryptPasswordEncoder;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String account= authentication.getName(); //获取用户名
String password= (String) authentication.getCredentials(); //获取密码
UserDetails userDetails= userServiceImpl.loadUserByUsername(account);
boolean checkPassword= bCryptPasswordEncoder.matches(password,userDetails.getPassword());
if(!checkPassword){
throw new BadCredentialsException("密码不正确,请重新登录!");
}
return new UsernamePasswordAuthenticationToken(account,password,userDetails.getAuthorities());
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
SpringsecurityConfig.java是springsecurity的配置,详细解析请看注释!!
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) //开启权限注解,默认是关闭的
public class SpringsecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
AuthenticationEnryPoint authenticationEnryPoint; //未登录
@Autowired
AuthenticationSuccess authenticationSuccess; //登录成功
@Autowired
AuthenticationFailure authenticationFailure; //登录失败
@Autowired
AuthenticationLogout authenticationLogout; //注销
@Autowired
AccessDeny accessDeny; //无权访问
@Autowired
SessionInformationExpiredStrategy sessionInformationExpiredStrategy; //检测异地登录
@Autowired
SelfAuthenticationProvider selfAuthenticationProvider; //自定义认证逻辑处理
@Bean
public UserDetailsService userDetailsService() {
return new UserServiceImpl();
}
//加密方式
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
//认证
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(selfAuthenticationProvider);
}
//授权
@Override
protected void configure(HttpSecurity http) throws Exception {
//cors()解决跨域问题,csrf()会与restful风格冲突,默认springsecurity是开启的,所以要disable()关闭一下
http.cors().and().csrf().disable();
// /index需要权限为ROLE_USER才能访问 /hello需要权限为ROLE_ADMIN才能访问
http.authorizeRequests()
.antMatchers("/index").hasRole("USER")
.antMatchers("/hello").hasRole("ADMIN")
.and()
.formLogin() //开启登录
.permitAll() //允许所有人访问
.successHandler(authenticationSuccess) // 登录成功逻辑处理
.failureHandler(authenticationFailure) // 登录失败逻辑处理
.and()
.logout() //开启注销
.permitAll() //允许所有人访问
.logoutSuccessHandler(authenticationLogout) //注销逻辑处理
.deleteCookies("JSESSIONID") //删除cookie
.and().exceptionHandling()
.accessDeniedHandler(accessDeny) //权限不足的时候的逻辑处理
.authenticationEntryPoint(authenticationEnryPoint) //未登录是的逻辑处理
.and()
.sessionManagement()
.maximumSessions(1) //最多只能一个用户登录一个账号
.expiredSessionStrategy(sessionInformationExpiredStrategy) //异地登录的逻辑处理
;
}
}
application.yml配置文件
server:
port: 80
spring:
datasource:
url: jdbc:mysql://localhost:3306/springsecurity_test?characterEncoding=utf8&serverTimezone=UTC
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
最终结果在上面就有,源码地址: https://gitee.com/liu-wenxin/springsecurity_demo.git , 使用 git clone https://gitee.com/liu-wenxin/springsecurity_demo.git 下载到本地。
上一篇: 详解PyTorch 安装指南
下一篇: 给小白整理的最后一篇 Python知识点