欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

SpringBoot 整合Spring Security安全框架 前后端分离(三)

程序员文章站 2022-06-13 15:18:16
...

导入主要的包。导入jpa持久化,web和security是必须的,还有hutool的工具包。
hutool工具包非常好用,推荐一下https://www.hutool.cn/docs/#/(官方文档)。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.1.4</version>
</dependency>

新建实体类

新建bean。UserInfo 实体类用于储存用户信息。

package com.security.bean;

import lombok.Data;

import java.util.List;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;

@Entity
@Data
public class UserInfo {

    @Id @GeneratedValue
    private long uid;//主键.
    private String username;//用户名.
    private String password;//密码.

    //用户--角色:多对多的关系.
    @ManyToMany(fetch=FetchType.EAGER)//立即从数据库中进行加载数据;
    @JoinTable(name = "UserPermission", joinColumns = { @JoinColumn(name = "uid") }, inverseJoinColumns ={@JoinColumn(name = "permission_id") })
    private List<Permission> permissions;

}

Permission 实体类用于储存权限信息。

package com.security.bean;

import lombok.Data;

import javax.persistence.*;
import java.util.List;

@Entity
@Data
public class Permission {
    @Id @GeneratedValue
    private long id;//主键.
    private String url;//授权链

}

UserToken 实体类用于储存用户token。为了主题明确就不用redis了。

package com.security.bean;

import lombok.Data;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
@Data
public class UserToken {
    @Id
    @GeneratedValue
    private long id;
    private String username;
    private String token;//令牌
}

这个实体类和数据库没有关系,是spring security要用到的。要继承security的User类。

package com.security.bean;

import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;

import java.util.Collection;

@Data
public class LoginUser extends User {

    private String token;//令牌

    public LoginUser(String username, String password, Collection<? extends GrantedAuthority> authorities) {
        super(username, password, authorities);
    }

    public LoginUser(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
        super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
    }
}

新建自定义的认证处理类

自定义过滤登录请求。请求登录接口之后会来到这里。

package com.security.filter;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response){
        System.out.println("+++++++++++++++++++++++++++++++++++++++++++++++++");
        //从请求参数中获取用户名和密码
        String username = request.getParameter("username");
        String password = request.getParameter("password");

        UsernamePasswordAuthenticationToken authRequest = null;
        try {
       		//该方法会去调用CustomUserDetailService.loadUserByUsername
            authRequest = new UsernamePasswordAuthenticationToken(username, password);
        }catch (Exception e) {
            e.printStackTrace();
            authRequest = new UsernamePasswordAuthenticationToken("", "");
        }
        setDetails(request, authRequest);
        return this.getAuthenticationManager().authenticate(authRequest);
    }
}

重写的loadUserByUsername会被spring security自动调用。

package com.security.config;

import cn.hutool.core.util.RandomUtil;
import com.security.bean.LoginUser;
import com.security.bean.Permission;
import com.security.bean.UserInfo;
import com.security.bean.UserToken;
import com.security.repository.UserInfoRepository;
import com.security.repository.UserTokenRepository;
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.Component;

import java.util.ArrayList;
import java.util.List;

@Component
public class CustomUserDetailService implements UserDetailsService{

    @Autowired
    private UserInfoRepository userInfoRepository;
    @Autowired
    private UserTokenRepository userTokenRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        System.out.println("=================================================");
        //通过username获取用户信息
        UserInfo userInfo = userInfoRepository.findByUsername(username);
        if(userInfo == null) {
            throw new UsernameNotFoundException("not found");
        }

        //定义权限列表.
        List<GrantedAuthority> authorities = new ArrayList<>();
        // 用户可以访问的资源名称(或者说用户所拥有的权限) 注意:必须"ROLE_"开头
        for(Permission permission:userInfo.getPermissions()) {
            authorities.add(new SimpleGrantedAuthority("ROLE_"+permission.getUrl()));
        }

        LoginUser userDetails = new LoginUser(username,userInfo.getPassword(),authorities);
        //将token也存入LoginUser 对象,之后要返回给前端。
        userDetails.setToken(createToken(username));
        return userDetails;
    }
    
    private String createToken(String username){
        //随便弄个token意思一下,实际的时候可以整点加密什么的。
        String token= RandomUtil.randomString(10);
        //将用户的token存入数据库
        UserToken userToken=userTokenRepository.findByUsername(username);
        if(userToken==null){
            userToken=new UserToken();
        }
        userToken.setUsername(username);
        userToken.setToken(token);
        userTokenRepository.save(userToken);
        return token;
    }
}

新建登录handler

登录成功之后会执行。

package com.security.handler;

import cn.hutool.json.JSONUtil;
import com.security.bean.LoginUser;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
        System.err.println("登录成功。。。");       
        response.setContentType("application/json;charset=UTF-8");
        // 以json的方式返回用户相关信息
        LoginUser user = (LoginUser)authentication.getPrincipal();
        System.err.println("用户信息:"+user);
        response.getWriter().write(JSONUtil.toJsonStr(user));
    }
}

登录失败之后会执行。

package com.security.handler;

import cn.hutool.json.JSONUtil;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException {
        System.err.println("登录失败。。。");
        // 以json的方式返回登录异常信息到前端
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(JSONUtil.toJsonStr(e.getMessage()));
    }
}

新建配置类

package com.security.config;

import com.security.filter.CustomAuthenticationFilter;
import com.security.handler.MyAuthenticationFailureHandler;
import com.security.handler.MyAuthenticationSuccessHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
//开启Spring Security的功能
@EnableWebSecurity
//添加@EnableGlobalMethodSecurity注解开启Spring方法级安全
// prePostEnabled 决定Spring Security的前注解是否可用 [@PreAuthorize,@PostAuthorize,..],设置为true
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and()
            .antMatcher("/**").authorizeRequests()
            .antMatchers("/", "/login**").permitAll()
            .anyRequest().authenticated()
            //这里必须要写formLogin(),不然原有的UsernamePasswordAuthenticationFilter不会出现,也就无法配置我们重新的UsernamePasswordAuthenticationFilter
            .and().formLogin().loginPage("/")
            .and().csrf().disable();
        //用重写的Filter替换掉原有的UsernamePasswordAuthenticationFilter
        http.addFilterAt(customAuthenticationFilter(),UsernamePasswordAuthenticationFilter.class);
    }

    /*
     * 注册自定义的UsernamePasswordAuthenticationFilter
     */
    @Bean
    public CustomAuthenticationFilter customAuthenticationFilter() throws Exception {
        CustomAuthenticationFilter filter = new CustomAuthenticationFilter();
        filter.setAuthenticationSuccessHandler(new MyAuthenticationSuccessHandler());
        filter.setAuthenticationFailureHandler(new MyAuthenticationFailureHandler());
        //自定义登录接口,请求时需要传入username和password。
        filter.setFilterProcessesUrl("/login");
        //这句很关键,重用WebSecurityConfigurerAdapter配置的AuthenticationManager,不然要自己组装AuthenticationManager
        filter.setAuthenticationManager(authenticationManagerBean());
        return filter;
    }

    /*
     * 指定加密方式
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

新建自定义的权限验证类

package com.security.config;

import com.security.bean.Permission;
import com.security.bean.UserInfo;
import com.security.bean.UserToken;
import com.security.repository.UserInfoRepository;
import com.security.repository.UserTokenRepository;
import com.security.utils.ServletUtils;
import com.security.utils.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;

/**
 * 权限验证类
 */
@Component
public class PermissionService {

    @Autowired
    private UserTokenRepository userTokenRepository;

    @Autowired
    private UserInfoRepository userInfoRepository;

    /**
     * 验证用户是否具备某权限
     * @param permission 权限字符串
     * @return 用户是否具备某权限
     */
    public boolean hasPermi(String permission){
        if (StringUtils.isEmpty(permission)){
            return false;
        }

        //获取token,直接放参数里面方便测试。
        String token=ServletUtils.getRequest().getParameter("token");
        UserToken userToken = userTokenRepository.findByToken(token);
        if(userToken==null){
            return false;
        }

        //当前用户权限列表
        UserInfo userInfo =userInfoRepository.findByUsername(userToken.getUsername());
        List<Permission> permissionList=userInfo.getPermissions();
        if (permissionList.size()==0){
            return false;
        }
        List<String> permissions=new ArrayList<>();
        for (Permission perm:permissionList){
            permissions.add(perm.getUrl());
        }

        System.err.println(permissions);
        return hasPermissions(permissions, permission);
    }

    /**
     * 判断是否包含权限
     * @param permissions 权限列表
     * @param permission 权限字符串
     * @return 用户是否具备某权限
     */
    private boolean hasPermissions(List<String> permissions, String permission){
        return permissions.contains(permission);
    }
}

开始测试

新建单元测试,只为往数据库加两个用户。密码要加密。

package com.security;

import com.security.bean.UserInfo;
import com.security.repository.UserInfoRepository;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest(classes = DemoApplication.class)
class DemoApplicationTests {

    @Autowired
    private UserInfoRepository userInfoRepository;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Test
    void contextLoads() {
        UserInfo admin = new UserInfo();
        admin.setUsername("admin");
        admin.setPassword(passwordEncoder.encode("123456"));
        userInfoRepository.save(admin);

        UserInfo user = new UserInfo();
        user.setUsername("user");
        user.setPassword(passwordEncoder.encode("123456"));
        userInfoRepository.save(user);
    }
}

运行一下这个单元测试,所有的表jpa都会自动生成。
自动生成了用户信息表,用户也添加上了。
SpringBoot 整合Spring Security安全框架 前后端分离(三)
自动生成的权限信息表,手动添加的数据。
SpringBoot 整合Spring Security安全框架 前后端分离(三)
自动生成了用户权限关系表,手动添加了两个用户的权限。SpringBoot 整合Spring Security安全框架 前后端分离(三)
三个表和起来看可以发现用户admin拥有admin权限和user权限,而用户user只拥有user权限。
最后还有个存用户token 的表,实际项目可以考虑用redis。
SpringBoot 整合Spring Security安全框架 前后端分离(三)
新建HelloController类。这个验证方式参考了若依的前后端分离框架,非常不错的框架,特别是在自动生成代码方面。
SpringBoot + Vue + Elemen UI感兴趣的朋友值得一看http://ruoyi.vip/(官方网站)。

package com.security.controller;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class HelloController {

    @GetMapping("/helloAdmin")
    @ResponseBody
    @PreAuthorize("@permissionService.hasPermi('helloAdmin')")
    public String helloAdmin(String token) {
        return "I am Admin";
    }

    @GetMapping("/helloUser")
    @ResponseBody
    @PreAuthorize("@permissionService.hasPermi('helloUser')")
    public String helloUser(String token) {
        return "I am User";
    }
}

请求登录接口http://localhost:8080/login?username=user&password=123456。为了方便测试,所有的接口都直接用GET请求了。

登录用户user
SpringBoot 整合Spring Security安全框架 前后端分离(三)
用户user可以访问helloUser接口。
SpringBoot 整合Spring Security安全框架 前后端分离(三)
用户user不能访问helloAdmin接口。

SpringBoot 整合Spring Security安全框架 前后端分离(三)
登录admin用户
SpringBoot 整合Spring Security安全框架 前后端分离(三)
admin用户两个接口都可访问。
SpringBoot 整合Spring Security安全框架 前后端分离(三)
SpringBoot 整合Spring Security安全框架 前后端分离(三)
真的是麻烦,以后还是用shiro吧。

退出登录

先新建一个退出成功handler。

package com.security.handler;

import cn.hutool.json.JSONUtil;
import com.security.bean.LoginUser;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        LoginUser user = (LoginUser) authentication.getPrincipal();
        String username = user.getUsername();
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(JSONUtil.toJsonStr("用户"+username+"退出登录"));
    }
}

然后在WebSecurityConfig 配置类的configure方法中加上下面这一行。

.and().logout().logoutSuccessHandler(new MyLogoutSuccessHandler())

退出的时候直接请求http://localhost:8080/logout,就用它自带的吧。自己生成的token就不用管了,下次登录会刷新。
SpringBoot 整合Spring Security安全框架 前后端分离(三)