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

导入主要的包。导入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);
        userDetails.setToken(createToken(username));
        return userDetails;
    }

    public String createToken(String username){
        //随便弄个token意思一下
        String token= RandomUtil.randomString(10);
        UserToken userToken=userTokenRepository.findByUsername(username);
        if(userToken==null){
            userToken=new UserToken();
        }
        userToken.setUsername(username);
        userToken.setToken(token);
        userTokenRepository.save(userToken);
        return token;
    }

    public void expireToken(String username){
        String token= RandomUtil.randomString(10);
        //这里没有删除,而是更新了token,让原来的失效
        UserToken userToken=userTokenRepository.findByUsername(username);
        userToken.setToken(token);
        userTokenRepository.save(userToken);
    }
}

新建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 CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
        System.err.println("登录成功。。。");
        // 以json的方式
        response.setContentType("application/json;charset=UTF-8");
        // 返回用户相关信息
        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.handler;

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

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

/**
 * 匿名未登录的时候访问,需要登录的资源的调用类
 */
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        //设置response状态码,返回错误信息等
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(JSONUtil.toJsonStr(e.getMessage()));
    }
}

没有权限时会执行。

package com.security.handler;

import cn.hutool.json.JSONUtil;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;

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

/**
 * 没有权限,被拒绝访问时的调用类
 */
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
        //设置response状态码,返回错误信息等
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(JSONUtil.toJsonStr(accessDeniedException.getMessage()));
    }
}

新建配置类

package com.security.config;

import com.security.filter.CustomAuthenticationFilter;
import com.security.handler.*;
import org.springframework.beans.factory.annotation.Autowired;
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 {
    @Autowired
    CustomLogoutSuccessHandler customLogoutSuccessHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable()
            //登录后,访问没有权限处理类
            .exceptionHandling().accessDeniedHandler(new CustomAccessDeniedHandler())
            //匿名访问,没有权限的处理类
            .authenticationEntryPoint(new CustomAuthenticationEntryPoint())

            .and()
            .antMatcher("/**").authorizeRequests()
            .antMatchers("/", "/login**").permitAll()
            .anyRequest().authenticated()

            //这里必须要写formLogin(),不然原有的UsernamePasswordAuthenticationFilter不会出现,也就无法配置我们重新的UsernamePasswordAuthenticationFilter
            .and().formLogin()
            //退出登录
            .and().logout().logoutSuccessHandler(customLogoutSuccessHandler);

        //用重写的Filter替换掉原有的UsernamePasswordAuthenticationFilter
        http.addFilterAt(customAuthenticationFilter(),UsernamePasswordAuthenticationFilter.class);
    }

    /*
     * 注册自定义的UsernamePasswordAuthenticationFilter
     */
    @Bean
    public CustomAuthenticationFilter customAuthenticationFilter() throws Exception {
        CustomAuthenticationFilter filter = new CustomAuthenticationFilter();
        filter.setAuthenticationSuccessHandler(new CustomAuthenticationSuccessHandler());
        filter.setAuthenticationFailureHandler(new CustomAuthenticationFailureHandler());
        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=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);
    }

    private HttpServletRequest getRequest() {
        return ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
    }
}

开始测试

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

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都会自动生成。
自动生成了用户信息表,用户也添加上了(重点不是权限管理,所以表就比较敷衍)。
在这里插入图片描述
自动生成的权限信息表,手动添加的数据。
在这里插入图片描述
自动生成了用户权限关系表,手动添加了两个用户的权限。在这里插入图片描述
三个表和起来看可以发现用户admin拥有admin权限和user权限,而用户user只拥有user权限。
最后还有个存用户token 的表,实际项目可以考虑用redis。
在这里插入图片描述
新建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
在这里插入图片描述
用户user可以访问helloUser接口。
在这里插入图片描述
用户user不能访问helloAdmin接口。

在这里插入图片描述
登录admin用户
在这里插入图片描述
admin用户两个接口都可访问。
在这里插入图片描述
在这里插入图片描述
真的是麻烦,以后还是用shiro吧。

退出登录

先新建一个退出成功handler,退出时让token失效。

package com.security.handler;

import cn.hutool.json.JSONUtil;
import com.security.bean.LoginUser;
import com.security.config.CustomUserDetailService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;

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

@Component
public class CustomLogoutSuccessHandler implements LogoutSuccessHandler{
    @Autowired
    CustomUserDetailService customUserDetailService;

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

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

.and().logout().logoutSuccessHandler(new CustomLogoutSuccessHandler())

退出的时候直接请求http://localhost:8080/logout,这是Spring Security自带的。
在这里插入图片描述

发布了82 篇原创文章 · 获赞 9 · 访问量 6176

猜你喜欢

转载自blog.csdn.net/weixin_43424932/article/details/104463458