1.spring security要想短时间内啃完对于我这种菜鸟而言是不可能的,所以只能一点一点的积累,先做个demo,这文篇讲得可以Sprngboot + Spring Security 实现前后端分离登录认证及权限控制。
2.这里是springsecurity的中文参考手册https://www.springcloud.cc/spring-security-zhcn.html,如下为本次springboot整合springsecurity入门案例的github地址:https://github.com/baisul/springbootsecurity.git 3.涉及到的表有如下,用户表,角色表,用户角色关联表,权限表,角色权限关联表,菜单表,权限菜单关联表
4.建表语句,以及插入部分数据
create table user(
id int(11) primary key auto_increment,
username varchar(36) default null comment'用户名',
password varchar(36) default null comment'密码',
email varchar(36) default null comment'邮箱',
sex int(11) default null comment'性别'
)
insert into user(username,password,email,sex) values('joker','e10adc3949ba59abbe56e057f20f883e','joker@163.com',1)
insert into user(username,password,email,sex) values('admin','202cb962ac59075b964b07152d234b70','admin@163.com',0)
insert into user(username,password,email,sex) values('neva','63a9f0ea7bb98050796b649e85481845','neva@163.com',1)
create table role(
id int(11) primary key auto_increment,
role_name varchar(36) default null comment'角色名称',
role_desc varchar(36) default null comment'角色描述'
)
insert into role(role_name,role_desc) values('超级管理员','拥有所有权限')
insert into role(role_name,role_desc) values('普通人员','拥有查看的权限')
insert into role(role_name,role_desc) values('管理员','拥有增删改查权限')
create table user_role_ref(
id int(11) primary key auto_increment,
user_id int(11) default null comment'用户id',
role_id int(11) default null comment'角色id'
)
insert into user_role_ref(user_id,role_id) values(1,2)
insert into user_role_ref(user_id,role_id) values(2,1)
insert into user_role_ref(user_id,role_id) values(3,3)
create table permission(
id int(11) primary key auto_increment,
code varchar(100) default null comment'权限code',
name varchar(100) default null comment'权限描述'
)
insert into permission(code,name) values('create_user','创建用户')
insert into permission(code,name) values('update_user','修改用户')
insert into permission(code,name) values('delete_user','删除用户')
insert into permission(code,name) values('query_user','查询用户')
create table role_permission_ref(
id int(11) primary key auto_increment,
role_id int(11) default null comment'角色id',
permission_id int(11) default null comment'权限id'
)
insert into role_permission_ref(role_id,permission_id) values(1,1)
insert into role_permission_ref(role_id,permission_id) values(1,2)
insert into role_permission_ref(role_id,permission_id) values(1,3)
insert into role_permission_ref(role_id,permission_id) values(1,4)
insert into role_permission_ref(role_id,permission_id) values(3,1)
insert into role_permission_ref(role_id,permission_id) values(3,2)
insert into role_permission_ref(role_id,permission_id) values(3,3)
insert into role_permission_ref(role_id,permission_id) values(3,4)
insert into role_permission_ref(role_id,permission_id) values(2,4)
create table menu(
id int(11) primary key auto_increment,
url varchar(100) default null comment'路径地址',
name varchar(30) default null comment'菜单名称',
content varchar(100) default null comment'描述',
state int(11) default null comment'是否可用,0可用,1禁用'
)
insert into menu(url,name,content,state) values('/user/getUsers','用户管理','用户列表',0)
insert into menu(url,name,content,state) values('/role/getRole','角色管理','角色列表',0)
insert into menu(url,name,content,state) values('/user/deleteUserById','用户管理','删除用户',0)
create table permission_menu_ref(
id int(11) primary key auto_increment,
permission_id int(11) default null comment'权限id',
menu_id int(11) default null comment'菜单id'
)
insert into permission_menu_ref(permission_id,menu_id) values(4,1)
insert into permission_menu_ref(permission_id,menu_id) values(3,3)
5.pom.xml文件以及application.properties文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.yl</groupId>
<artifactId>springbootsecurity</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springbootsecurity</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<!-- <spring.security.version>5.1.6.RELEASE</spring.security.version>-->
<fastjson.version>1.2.46</fastjson.version>
<druid.version>1.1.12</druid.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</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>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.8.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<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-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
# Spring boot application
spring.application.name=springboot-security
server.port=8090
server.servlet.context-path=/sbs
spring.datasource.driverClassName=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/spring_security?useunicode=true;&serverTimezone=GMT&characterEncoding=utf8
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.initialSize=5
spring.datasource.minIdle=5
spring.datasource.maxActive=20
spring.datasource.maxWait=60000
spring.datasource.timeBetweenEvictionRunsMillis=60000
spring.datasource.minEvictableIdleTimeMillis=300000
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8
#mybatsi,xml扫描
mybatis.mapper-locations=classpath*:mapper/*.xml
6.项目目录结构
7.常量类(utils包)
package com.yl.springbootsecurity.utils;
/**
* 存放常量
*/
public interface Constant {
/**
* 状态成功码
*/
public static final int SUCCESS_CODE = 200;
/**
* 状态失败码
*/
public static final int FAIL_CODE = 0;
/**
* 操作成功
*/
public static final boolean OPERATE_SUCCESS = true;
/**
* 操作失败
*/
public static final boolean OPERATE_ERROR = false;
public static final String USER_NOT_LOGIN = "用户未登录,请先登录";
public static final String USER_NOT_EXIT = "用户不存在";
public static final String USER_PASSWORD_ERROR = "密码输入出错";
public static final String COMMON_ERROR = "系统出错";
public static final String ACCOUNT_PASS_TIME = "账号过期";
public static final String ACCOUNT_LOCK = "账号锁定";
public static final String ACCOUNT_NOT_USE = "账号不可用";
public static final String USER_PASSWORD_PASS_TIME = "密码过期";
}
8.返回结果类(utils包)
package com.yl.springbootsecurity.utils;
import java.io.Serializable;
/**
* 公用的结果返回
*/
public class ResultModel implements Serializable {
/**
* 状态码:200成功,0失败
*/
private Integer code;
/**
* 返回信息
*/
private String msg;
/**
* 操作是否成功标志
*/
private Boolean flag;
/**
* 返回数据
*/
private Object data;
public ResultModel(Integer code) {
this.code = code;
}
public ResultModel(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
public ResultModel(Integer code, String msg, Boolean flag) {
this.code = code;
this.msg = msg;
this.flag = flag;
}
public ResultModel(Integer code, String msg, Object data) {
this.code = code;
this.msg = msg;
this.data = data;
}
public ResultModel(Integer code, String msg, Boolean flag, Object data) {
this.code = code;
this.msg = msg;
this.flag = flag;
this.data = data;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public Boolean getFlag() {
return flag;
}
public void setFlag(Boolean flag) {
this.flag = flag;
}
@Override
public String toString() {
return "ResultModel{" +
"code=" + code +
", msg='" + msg + '\'' +
", flag=" + flag +
", data=" + data +
'}';
}
}
9.简单的md5util(utils包)
package com.yl.springbootsecurity.utils;
import org.springframework.util.DigestUtils;
/**
* md5加密解密工具类
*/
public class Md5Util {
//加密
public static String encode(String str) {
return DigestUtils.md5DigestAsHex(str.getBytes());
}
//匹配是否正确
public static boolean isMatch(String ostr,String nstr) {
if (encode(ostr).equals(nstr)) {
return true;
} else {
return false;
}
}
}
10.实体类,user和permission(entity包),以下暂且都是先用到这两个实体类,来测试
package com.yl.springbootsecurity.entity;
import java.io.Serializable;
/**
* 用户表实体类
*/
public class User implements Serializable {
private Integer id;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 邮箱
*/
private String email;
/**
* 性别
*/
private Integer sex;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Integer getSex() {
return sex;
}
public void setSex(Integer sex) {
this.sex = sex;
}
}
package com.yl.springbootsecurity.entity;
import java.io.Serializable;
/**
* 权限表实体类
*/
public class Permission implements Serializable {
private Integer id;
/**
* 权限编码
*/
private String code;
/**
* 描述
*/
private String name;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
11.mapper接口(mapper包)
package com.yl.springbootsecurity.mapper;
import com.yl.springbootsecurity.entity.User;
import java.util.List;
public interface UserMapper {
User getUserByUserName(String username);
List<User> getUsers();
Integer deleteUserById(Integer id);
}
package com.yl.springbootsecurity.mapper;
import com.yl.springbootsecurity.entity.Permission;
import java.util.List;
public interface PermissionMapper {
List<Permission> getPermissionsByUserId(Integer userId);
List<Permission> selectListByPath(String path);
}
12.mapper映射文件(resources/mapper)
<?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.yl.springbootsecurity.mapper.UserMapper">
<select id="getUserByUserName" parameterType="java.lang.String" resultType="com.yl.springbootsecurity.entity.User">
select
*
from user
where username = #{username}
</select>
<select id="getUsers" resultType="com.yl.springbootsecurity.entity.User">
select * from user
</select>
<delete id="deleteUserById" parameterType="java.lang.Integer">
delete from user where id = #{id}
</delete>
</mapper>
<?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.yl.springbootsecurity.mapper.PermissionMapper">
<select id="getPermissionsByUserId" parameterType="java.lang.Integer" resultType="com.yl.springbootsecurity.entity.Permission">
select
a.* from permission a
left join role_permission_ref b on a.id = b.permission_id
left join role c on b.role_id = c.id
left join user_role_ref d on d.role_id = c.id
left join user f on f.id = d.user_id
where f.id = #{userId}
</select>
<select id="selectListByPath" parameterType="java.lang.String" resultType="com.yl.springbootsecurity.entity.Permission">
select a. *
from permission a
left join permission_menu_ref b on a.id = b.permission_id
left join menu c on b.menu_id = c.id
where c.url = #{path}
</select>
</mapper>
13.service接口(包service)
package com.yl.springbootsecurity.service;
import com.yl.springbootsecurity.entity.User;
import java.util.List;
public interface UserService {
/**
* 根据用户名获取用户
* @param username
* @return
*/
User getUserByUserName(String username);
/**
* 获取用户列表信息
* @return
*/
List<User> getUsers();
/**
* 根据ID删除用户
* @param id
* @return
*/
Integer deleteUserById(Integer id);
}
package com.yl.springbootsecurity.service;
import com.yl.springbootsecurity.entity.Permission;
import java.util.List;
public interface PermissionService {
/**
* 根据用户ID获取权限列表
* @param userId
* @return
*/
List<Permission> getPermissionsByUserId(Integer userId);
/**
* 根据菜单路径去获取权限列表
* @param path
* @return
*/
List<Permission> selectListByPath(String path);
}
14.service实现类(sevice.impl包)
package com.yl.springbootsecurity.service.impl;
import com.yl.springbootsecurity.entity.User;
import com.yl.springbootsecurity.mapper.UserMapper;
import com.yl.springbootsecurity.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 用户表服务实现类
*/
@Service
public class UserServiceImpl implements UserService {
//属性注入
@Autowired
private UserMapper userMapper;
@Override
public User getUserByUserName(String username) {
return userMapper.getUserByUserName(username);
}
@Override
public List<User> getUsers() {
return userMapper.getUsers();
}
@Override
public Integer deleteUserById(Integer id) {
return userMapper.deleteUserById(id);
}
}
package com.yl.springbootsecurity.service.impl;
import com.yl.springbootsecurity.entity.Permission;
import com.yl.springbootsecurity.mapper.PermissionMapper;
import com.yl.springbootsecurity.service.PermissionService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class PermissionServiceImpl implements PermissionService {
@Autowired
private PermissionMapper permissionMapper;
@Override
public List<Permission> getPermissionsByUserId(Integer userId) {
return permissionMapper.getPermissionsByUserId(userId);
}
@Override
public List<Permission> selectListByPath(String path) {
return permissionMapper.selectListByPath(path);
}
}
15.controller层(controller包)
package com.yl.springbootsecurity.controller;
import com.yl.springbootsecurity.entity.User;
import com.yl.springbootsecurity.service.UserService;
import com.yl.springbootsecurity.utils.Constant;
import com.yl.springbootsecurity.utils.ResultModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 用户表控制器
*/
@RestController
@RequestMapping("/user")
public class UserController {
//属性注入
@Autowired
private UserService userService;
/**
* 获取用户列表
* @return
*/
@GetMapping("/getUsers")
public ResultModel getUsers() {
List<User> users = this.userService.getUsers();
ResultModel resultModel;
if (users.size() > 0) {
resultModel = new ResultModel(Constant.SUCCESS_CODE,"查找成功",true,users);
} else {
resultModel = new ResultModel(Constant.SUCCESS_CODE,"暂无数据",true,null);
}
return resultModel;
}
/**
* 根据用户ID删除用户
* @param id
* @return
*/
@DeleteMapping("/deleteUserById")
public ResultModel deleteUserById(@RequestParam(required = true) Integer id){
ResultModel resultModel;
Integer result = this.userService.deleteUserById(id);
if (result > 0) {
resultModel = new ResultModel(200,"删除成功");
} else {
resultModel = new ResultModel(0,"删除失败");
}
return resultModel;
}
}
16.以上的常规操作做完后,接下来就是关于security的一些工作了
1)security的核心配置类(config包)
package com.yl.springbootsecurity.config;
import com.yl.springbootsecurity.filter.CustomizeAbstractSecurityInterceptor;
import com.yl.springbootsecurity.filter.CustomizeAccessDecisionManager;
//import com.yl.springbootsecurity.filter.CustomizeAuthenticationFilter;
import com.yl.springbootsecurity.filter.CustomizeFilterInvocationSecurityMetadataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* security核心配置类,继承WebSecurityConfigurerAdapter
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//注入自定义认证提供者
@Autowired
private CustomizeAuthenticationProvider customizeAuthenticationProvider;
//注入认证管理器
@Override
@Bean
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
//注入登录成功的处理器
@Bean
public LoginSuccessHandler getLoginSuccessHandler() {
return new LoginSuccessHandler();
}
//注入登录失败的处理器
@Bean
public LoginFailHandler getLoginFailHandler() {
return new LoginFailHandler();
}
//注入未登录的异常处理器
@Bean
public CustomizeAuthenticationEntryPoint getCustomizeAuthenticationEntryPoint(){
return new CustomizeAuthenticationEntryPoint();
}
//注入登出成功的处理器
@Bean
public CustomizeLogoutSuccessHandler getCustomizeLogOutSuccessHandler() {
return new CustomizeLogoutSuccessHandler();
}
//注册自定义的UsernamePasswordAuthenticationFilter
// @Bean
// public CustomizeAuthenticationFilter getCustomizeAuthenticationFilter() throws Exception {
// CustomizeAuthenticationFilter filter = new CustomizeAuthenticationFilter();
// filter.setAuthenticationSuccessHandler(getLoginSuccessHandler());
// filter.setAuthenticationFailureHandler(getLoginFailHandler());
filter.setFilterProcessesUrl("/login/self");
// filter.setFilterProcessesUrl("/authentication/form");
//
// //这句很关键,重用WebSecurityConfigurerAdapter配置的AuthenticationManager,不然要自己组装AuthenticationManager
// filter.setAuthenticationManager(authenticationManagerBean());
// return filter;
// }
//访问决策管理器
@Autowired
CustomizeAccessDecisionManager accessDecisionManager;
//实现权限拦截
@Autowired
CustomizeFilterInvocationSecurityMetadataSource securityMetadataSource;
@Autowired
private CustomizeAbstractSecurityInterceptor securityInterceptor;
@Override
protected void configure(HttpSecurity http) throws Exception {
//1.配置授权方式,这个configure方法里面主要是配置一些
//http的相关配置,包括登入,登出,异常处理,会话管理等
http.authorizeRequests().
withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O o) {
o.setAccessDecisionManager(accessDecisionManager);//访问决策管理器
o.setSecurityMetadataSource(securityMetadataSource);//安全元数据源
return o;
}
})
//登陆页面,登陆请求,登出请求,任何人都可以访问
.antMatchers("/login","/authentication/form","/logout").permitAll()
//动态加载权限时,这里注释掉
.antMatchers("/user/getUsers").hasAuthority("query_user")
.antMatchers("/user/deleteUserById").hasAuthority("delete_user")
.anyRequest().authenticated()//其他所有请求都要认证
//登入
.and().formLogin()
.permitAll() //允许所有人访问
.loginProcessingUrl("/authentication/form") //登录成功后,处理登录的url
.successHandler(getLoginSuccessHandler()) //登录成功后,调用成功处理器
.failureHandler(getLoginFailHandler()) //登录失败,//调用失败处理器
//登出
.and().logout()
.permitAll()
.logoutSuccessUrl("/logout")
.logoutSuccessHandler(getCustomizeLogOutSuccessHandler()) //登出成功后,调用登出成功处理器
.deleteCookies("JSESSIONID")//登出之后删除cookie
.and().headers()
//异常处理(权限拒绝,登录失效)
.and().exceptionHandling()
//匿名用户访问无权限资源时的异常处理
.authenticationEntryPoint(getCustomizeAuthenticationEntryPoint()); //异常处理器
//用重写的Filter替换掉原有的UsernamePasswordAuthenticationFilter
// http.addFilterAt(getCustomizeAuthenticationFilter(),
// UsernamePasswordAuthenticationFilter.class);
http.addFilterBefore(securityInterceptor, FilterSecurityInterceptor.class);//增加到默认拦截链中
http.csrf().disable();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//2.配置认证方式
//使用自定义的认证管理器
auth.authenticationProvider(this.customizeAuthenticationProvider);
}
}
2)自定义认证提供者类CustomizeAuthenticationProvider,这个类要实先Authentication接口,并且重写authentication这个方法,密码加密后比较还有根据用户获取权限列表可以在这个方法里面做
package com.yl.springbootsecurity.config;
import com.yl.springbootsecurity.entity.Permission;
import com.yl.springbootsecurity.service.PermissionService;
import com.yl.springbootsecurity.service.UserService;
import com.yl.springbootsecurity.utils.Md5Util;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
* 自定义认证提供者,对用户名和密码进行验证
* @author Administrator
*/
@Service
public class CustomizeAuthenticationProvider implements AuthenticationProvider{
@Autowired
private UserService userService;
@Autowired
private PermissionService permissionService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
//获取前端传过来的的username和password
String username = authentication.getName();
String prepassword = (String)authentication.getCredentials();
//定义UserDetails对象,构造再返回
UserDetails userDetails = null;
com.yl.springbootsecurity.entity.User user = this.userService.getUserByUserName(username);
if (user == null) {
throw new InternalAuthenticationServiceException("用户不存在");
}
//根据用户名获取权限列表
List<Permission> permissions= permissionService.getPermissionsByUserId(user.getId());
List<GrantedAuthority> authorities = new ArrayList<>();
if (permissions != null && permissions.size() > 0) {
permissions.stream().forEach(item -> {
GrantedAuthority grantedAuthority= new SimpleGrantedAuthority(item.getCode());
authorities.add(grantedAuthority);
});
}
//构造UserDetails对象
userDetails = new User(username, user.getPassword(), authorities);
if (authentication.getCredentials() == null) {
throw new BadCredentialsException("登录名或密码错误");
//获取前端传过来的密码,经过加密之后和数据库保存的密码比较是否一致
} else if (!Md5Util.encode(prepassword).equals(user.getPassword())) {
throw new BadCredentialsException("密码错误");
}
//返回UsernamePasswordAuthenticationToken对象
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(userDetails, authentication.getCredentials(), userDetails.getAuthorities());
return result;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
3)登陆成功处理器,要实现AuthenticationSuccessHandler
package com.yl.springbootsecurity.config;
import com.alibaba.fastjson.JSON;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yl.springbootsecurity.utils.Constant;
import com.yl.springbootsecurity.utils.ResultModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
*
* 登陆成功处理器
*/
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
private ObjectMapper objectMapper;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
//处理中文乱码以及跨域问题
response.setContentType("application/json;charset=UTF-8");
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT, OPTIONS, DELETE");
//得到用户
User user = (User)authentication.getPrincipal();
//这里可以做一些逻辑处理,比如,要返回这个用户的菜单权限到前端,前端拿到后,动态加载显示
//返回数据
ResultModel resultModel = new ResultModel(Constant.SUCCESS_CODE,"查找成功",Constant.OPERATE_SUCCESS,user);
//以流的方式写数据到前端
response.getWriter().write(JSON.toJSONString(resultModel));
}
}
4)登录失败处理器,要实现AuthenticationFailureHandler
package com.yl.springbootsecurity.config;
import com.alibaba.fastjson.JSON;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yl.springbootsecurity.utils.Constant;
import com.yl.springbootsecurity.utils.ResultModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.*;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
*
* 登陆失败处理器
*/
public class LoginFailHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException e) throws IOException, ServletException {
ResultModel resultModel;
response.setContentType("application/json;charset=utf-8");
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT, OPTIONS, DELETE");
if (e instanceof InternalAuthenticationServiceException) {
//用户不存在
resultModel = new ResultModel(Constant.FAIL_CODE,Constant.USER_NOT_EXIT,Constant.OPERATE_ERROR);
} else if (e instanceof BadCredentialsException) {
//密码错误
resultModel = new ResultModel(Constant.FAIL_CODE,Constant.USER_PASSWORD_ERROR,Constant.OPERATE_ERROR);
} else if (e instanceof AccountExpiredException) {
//账号过期
resultModel = new ResultModel(Constant.FAIL_CODE,Constant.ACCOUNT_PASS_TIME,Constant.OPERATE_ERROR);
} else if (e instanceof CredentialsExpiredException) {
//密码过期
resultModel = new ResultModel(Constant.FAIL_CODE,Constant.USER_PASSWORD_PASS_TIME,Constant.OPERATE_ERROR);
} else if (e instanceof DisabledException) {
//账号不可用
resultModel = new ResultModel(Constant.FAIL_CODE,Constant.ACCOUNT_NOT_USE,Constant.OPERATE_ERROR);
} else if (e instanceof LockedException) {
//账号锁定
resultModel = new ResultModel(Constant.FAIL_CODE,Constant.ACCOUNT_LOCK,Constant.OPERATE_ERROR);
}else {
resultModel = new ResultModel(Constant.FAIL_CODE,Constant.COMMON_ERROR,Constant.OPERATE_ERROR);
}
response.getWriter().write(JSON.toJSONString(resultModel));
}
}
5)匿名用户访问时无权限的异常处理器,要实现AuthenticationEntryPoint
package com.yl.springbootsecurity.config;
import com.alibaba.fastjson.JSON;
import com.yl.springbootsecurity.utils.Constant;
import com.yl.springbootsecurity.utils.ResultModel;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 匿名用户访问无权限时的异常
*/
@Component
public class CustomizeAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
ResultModel resultModel = new ResultModel(Constant.FAIL_CODE,Constant.USER_NOT_LOGIN);
httpServletResponse.setHeader("Access-Control-Allow-Origin", "*");
httpServletResponse.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT, OPTIONS, DELETE");
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.getWriter().write(JSON.toJSONString(resultModel));
}
}
6)登出成功处理器,要实现LogoutSuccessHandler
package com.yl.springbootsecurity.config;
import com.alibaba.fastjson.JSON;
import com.yl.springbootsecurity.utils.Constant;
import com.yl.springbootsecurity.utils.ResultModel;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.User;
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 CustomizeLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//处理中文乱码及解决乱码
response.setContentType("application/json;charset=utf-8");
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT, OPTIONS, DELETE");
//返回数据
ResultModel resultModel = new ResultModel(Constant.SUCCESS_CODE,"登出成功",Constant.OPERATE_SUCCESS);
//以流的方式写数据到前端
response.getWriter().write(JSON.toJSONString(resultModel));
}
}
17.测试一,这里先用postman来做测试
1)启动springboot主程序
2)访问删除用户接口,应该提示未登录,请先登录
3)在建表,插入数据的时候,我们加了三个用户,admin这个用户为超级管理员,拥有所有的权限,而joker这个用户只有查询的权限
4)以admin身份登录时,拥有所有的权限
调查询接口可用
调删除接口可用
5)以joker身份登录时,只有查询的权限,这时候记得先登出,如下不再重复,只要是切换用户身份,第一步得先登出
调查询接口时可用
调删除接口时拒绝访问
18.在上面,我们首先是根据用户名去拿到用户信息,然后根据用户ID去获取其拥有的权限列表,还在配置类的configure方法那里配置了哪一些角色才可以访问某一一些请求路径,如下图:
19.那往后,我们还要加一些权限控制的话还需要手动回来这里加,不够灵活,所以可以搞成动态的,主要思路是参考上述的文章,里面说到AccessDecisionManager,即访问决策管理器,我们要写一个类实现它,其主要是用来对我们访问资源时做判断,如果有权限就放行,没权限就拒绝访问,在这之前,还要拦截到用户发起的请求,根据前端传来的url,去数据库查询当前url对应的权限列表,然后再交给访问决策器,这里涉及到另外一个东西,SecurityMetadataSource即安全元数据源,我们用它的一个子类FilterInvocationSecurityMetadataSource来完成上述操作,以上两个完成后,还有编写一个拦截器,增加到springsecurity的拦截器链中,最后还要在security的核心配置类,配置。
webSecurityConfig类最终如下
package com.yl.springbootsecurity.config;
import com.yl.springbootsecurity.filter.CustomizeAbstractSecurityInterceptor;
import com.yl.springbootsecurity.filter.CustomizeAccessDecisionManager;
//import com.yl.springbootsecurity.filter.CustomizeAuthenticationFilter;
import com.yl.springbootsecurity.filter.CustomizeFilterInvocationSecurityMetadataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* security核心配置类,继承WebSecurityConfigurerAdapter
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//注入自定义认证提供者
@Autowired
private CustomizeAuthenticationProvider customizeAuthenticationProvider;
//注入认证管理器
@Override
@Bean
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
//注入登录成功的处理器
@Bean
public LoginSuccessHandler getLoginSuccessHandler() {
return new LoginSuccessHandler();
}
//注入登录失败的处理器
@Bean
public LoginFailHandler getLoginFailHandler() {
return new LoginFailHandler();
}
//注入未登录的异常处理器
@Bean
public CustomizeAuthenticationEntryPoint getCustomizeAuthenticationEntryPoint(){
return new CustomizeAuthenticationEntryPoint();
}
//注入登出成功的处理器
@Bean
public CustomizeLogoutSuccessHandler getCustomizeLogOutSuccessHandler() {
return new CustomizeLogoutSuccessHandler();
}
//注册自定义的UsernamePasswordAuthenticationFilter
// @Bean
// public CustomizeAuthenticationFilter getCustomizeAuthenticationFilter() throws Exception {
// CustomizeAuthenticationFilter filter = new CustomizeAuthenticationFilter();
// filter.setAuthenticationSuccessHandler(getLoginSuccessHandler());
// filter.setAuthenticationFailureHandler(getLoginFailHandler());
filter.setFilterProcessesUrl("/login/self");
// filter.setFilterProcessesUrl("/authentication/form");
//
// //这句很关键,重用WebSecurityConfigurerAdapter配置的AuthenticationManager,不然要自己组装AuthenticationManager
// filter.setAuthenticationManager(authenticationManagerBean());
// return filter;
// }
//访问决策管理器
@Autowired
CustomizeAccessDecisionManager accessDecisionManager;
//实现权限拦截
@Autowired
CustomizeFilterInvocationSecurityMetadataSource securityMetadataSource;
@Autowired
private CustomizeAbstractSecurityInterceptor securityInterceptor;
@Override
protected void configure(HttpSecurity http) throws Exception {
//1.配置授权方式,这个configure方法里面主要是配置一些
//http的相关配置,包括登入,登出,异常处理,会话管理等
http.authorizeRequests().
withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O o) {
o.setAccessDecisionManager(accessDecisionManager);//访问决策管理器
o.setSecurityMetadataSource(securityMetadataSource);//安全元数据源
return o;
}
})
//登陆页面,登陆请求,登出请求,任何人都可以访问
.antMatchers("*/login","/authentication/form","/logout","*/home").permitAll()
//动态加载权限时,这里注释掉
// .antMatchers("/user/getUsers").hasAuthority("query_user")
// .antMatchers("/user/deleteUserById").hasAuthority("delete_user")
.anyRequest().authenticated()//其他所有请求都要认证
//登入
.and().formLogin()
.permitAll() //允许所有人访问
.loginProcessingUrl("/authentication/form") //登录成功后,处理登录的url
.successHandler(getLoginSuccessHandler()) //登录成功后,调用成功处理器
.failureHandler(getLoginFailHandler()) //登录失败,//调用失败处理器
//登出
.and().logout()
.permitAll()
.logoutSuccessUrl("/logout")
.logoutSuccessHandler(getCustomizeLogOutSuccessHandler()) //登出成功后,调用登出成功处理器
.deleteCookies("JSESSIONID")//登出之后删除cookie
.and().headers()
//异常处理(权限拒绝,登录失效)
.and().exceptionHandling()
//匿名用户访问无权限资源时的异常处理
.authenticationEntryPoint(getCustomizeAuthenticationEntryPoint()); //异常处理器
//用重写的Filter替换掉原有的UsernamePasswordAuthenticationFilter
// http.addFilterAt(getCustomizeAuthenticationFilter(),
// UsernamePasswordAuthenticationFilter.class);
http.addFilterBefore(securityInterceptor, FilterSecurityInterceptor.class);//增加到默认拦截链中
http.csrf().disable();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//2.配置认证方式
//使用自定义的认证管理器
auth.authenticationProvider(this.customizeAuthenticationProvider);
}
}
20.权限拦截器
package com.yl.springbootsecurity.filter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.SecurityMetadataSource;
import org.springframework.security.access.intercept.AbstractSecurityInterceptor;
import org.springframework.security.access.intercept.InterceptorStatusToken;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Service;
import javax.servlet.*;
import java.io.IOException;
/**
* 权限拦截器
*/
@Service
public class CustomizeAbstractSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
@Autowired
private FilterInvocationSecurityMetadataSource securityMetadataSource;
@Autowired
public void setMyAccessDecisionManager(CustomizeAccessDecisionManager accessDecisionManager) {
super.setAccessDecisionManager(accessDecisionManager);
}
@Override
public Class<?> getSecureObjectClass() {
return FilterInvocation.class;
}
@Override
public SecurityMetadataSource obtainSecurityMetadataSource() {
return this.securityMetadataSource;
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(servletRequest, servletResponse, filterChain);
invoke(fi);
}
public void invoke(FilterInvocation fi) throws IOException, ServletException {
//fi里面有一个被拦截的url
//里面调用MyInvocationSecurityMetadataSource的getAttributes(Object object)这个方法获取fi对应的所有权限
//再调用MyAccessDecisionManager的decide方法来校验用户的权限是否足够
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
//执行下一个拦截器
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
super.afterInvocation(token, null);
}
}
}
21.安全元数据源,CustomizeFilterInvocationSecurityMetadataSource
package com.yl.springbootsecurity.filter;
import com.yl.springbootsecurity.entity.Permission;
import com.yl.springbootsecurity.service.PermissionService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import java.util.Collection;
import java.util.List;
/**
* 安全元数据源FilterInvocationSecurityMetadataSource,主要是拦截到请求路径,做相关处理
*/
@Component
public class CustomizeFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
AntPathMatcher antPathMatcher = new AntPathMatcher();
@Autowired
private PermissionService permissionService;
@Override
public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
//获取请求地址
String requestUrl = ((FilterInvocation) o).getRequestUrl();
List<Permission> permissionList;
//根据请求地址获取该地址有哪一些权限才能够访问
//不是get请求的情况,直接查数据
if (!requestUrl.contains("?")) {
permissionList = permissionService.selectListByPath(requestUrl);
} else {
//为get请求的情况下,要切割字符串,因为?后面带的是一些参数,我们在数据库保存的仅仅是请求地址
String [] arrs = requestUrl.split("\\?");
permissionList = permissionService.selectListByPath(arrs[0]);
}
if(permissionList == null || permissionList.size() == 0){
//请求路径没有配置权限,表明该请求接口可以任意访问
return null;
}
//如果当前请求地址需要权限才能够访问
String[] attributes = new String[permissionList.size()];
for(int i = 0;i<permissionList.size();i++){
attributes[i] = permissionList.get(i).getCode();
}
//调用Securtity的createList方法,构造出一个List<ConfigAttribute>的集合
//因为这个集合在访问决策管理器要用到
return SecurityConfig.createList(attributes);
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
22.访问决策管理器,CustomizeAccessDecisionManager
package com.yl.springbootsecurity.filter;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.Iterator;
/**
* 访问决策管理器
*/
@Component
public class CustomizeAccessDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
Iterator<ConfigAttribute> iterator = collection.iterator();
while (iterator.hasNext()) {
ConfigAttribute ca = iterator.next();
//当前请求需要的权限
String needRole = ca.getAttribute();
//当前用户所具有的权限
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
//如果当前用户拥有当前请求的权限则放行
for (GrantedAuthority authority : authorities) {
if (authority.getAuthority().equals(needRole)) {
return;
}
}
}
//没有权限则抛异常
throw new AccessDeniedException("权限不足!");
}
@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
23.测试二,动态权限访问的测试,这里测试还是用postman,只是把请求地址和授权角色写死的那一块注释掉,动态地来实现认证,授权
1)以admin身份登录
调用用户列表接口成功
调用用户删除接口成功
2)以joker身份登录,记得先登出
调用用户列表接口成功
调用用户删除接口,拒绝
24.至此。案例已完成,本想着结合vue来做测试的,但测试时有点问题,往后有时间再做吧