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自带的。