spring-security详解
随便写一下,增删改查返回对应的字符
import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@RestController@RequestMapping("/security")public class OauthBase { @GetMapping("/add") public String add(){ return "add"; } @GetMapping("/update") public String update(){ return "update"; } @GetMapping("/get") public String get(){ return "get"; } @GetMapping("/del") public String del(){ return "del"; }}
基础认证配置类
import org.springframework.context.annotation.Bean;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.crypto.password.NoOpPasswordEncoder;import org.springframework.stereotype.Component;@Component@EnableWebSecuritypublic class SecurityConfig extends WebSecurityConfigurerAdapter { /** * 新增Security * 授权账户 * @param auth * @throws Exception */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { /** * 账号、密码、接口权限 */ auth.inMemoryAuthentication().withUser("mykt").password("mykt").authorities("/"); } /** * 认证方式 * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { /** * 认证之后就能访问所有接口 */ http.authorizeRequests().antMatchers("/**").fullyAuthenticated().and().httpBasic(); } /** * 加密方式,恢复以前模式 * @return */ @Bean public static NoOpPasswordEncoder PasswordEncoder(){ return (NoOpPasswordEncoder)NoOpPasswordEncoder.getInstance(); }}
输入对应的账号密码就可以访问对应的接口,这个非常简单。
From表单验证
/** * 认证方式 * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { /** * 基础认证 */ //http.authorizeRequests().antMatchers("/**").fullyAuthenticated().and().httpBasic(); /** * form表单验证 */ http.authorizeRequests().antMatchers("/**").fullyAuthenticated().and().formLogin(); }
效果图(账号密码没变,页面自带,不需要自己写)
配置权限规则(不同的账号对应不同的权限)
import org.springframework.context.annotation.Bean;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.crypto.password.NoOpPasswordEncoder;import org.springframework.stereotype.Component;@Component@EnableWebSecuritypublic class SecurityConfig extends WebSecurityConfigurerAdapter { /** * 新增Security * 授权账户 * @param auth * @throws Exception */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { /** * 账号、密码、接口权限 */ auth.inMemoryAuthentication().withUser("mykt-admin").password("mykt-admin").authorities("add","update","get","del"); auth.inMemoryAuthentication().withUser("mykt-add").password("mykt-add").authorities("add"); auth.inMemoryAuthentication().withUser("mykt-update").password("mykt-update").authorities("update"); auth.inMemoryAuthentication().withUser("mykt-get").password("mykt-get").authorities("get"); auth.inMemoryAuthentication().withUser("mykt-del").password("mykt-del").authorities("del"); } /** * 认证方式 * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { /** * 基础认证 */ //http.authorizeRequests().antMatchers("/**").fullyAuthenticated().and().httpBasic(); /** * form表单验证 */ //http.authorizeRequests().antMatchers("/**").fullyAuthenticated().and().formLogin(); /** * 拦截规则 */ http.authorizeRequests().antMatchers("/add").hasAnyAuthority("add") .antMatchers("/get").hasAnyAuthority("get") .antMatchers("/update").hasAnyAuthority("update") .antMatchers("/del").hasAnyAuthority("del") //form验证 .antMatchers("/**").fullyAuthenticated().and().formLogin(); } /** * 加密方式,恢复以前模式 * @return */ @Bean public static NoOpPasswordEncoder PasswordEncoder(){ return (NoOpPasswordEncoder)NoOpPasswordEncoder.getInstance(); }}
创建了五个账号,mykt-admin、mykt-add 、mykt-update 、mykt-get 、mykt-del,那么从账号名称中就可以看出来这些账号对应的权限
mykt-add测试
mykt-admin测试
权限不足跳转页面
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;import org.springframework.boot.web.server.ErrorPage;import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.http.HttpStatus;@Configurationpublic class WebServerAutoConfiguration { @Bean public ConfigurableServletWebServerFactory webServerFactroy(){ TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory(); ErrorPage errorPage403 = new ErrorPage(HttpStatus.FORBIDDEN,"/error/403"); ErrorPage errorPage404 = new ErrorPage(HttpStatus.NOT_FOUND,"/error/404"); ErrorPage errorPage500 = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR,"/error/500"); factory.addErrorPages(errorPage403,errorPage404,errorPage500); return factory; }}
import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@RestControllerpublic class ErrorController { @RequestMapping("/error/403") public String error403(){ return "你当前访问的接口权限不足"; } @RequestMapping("/error/404") public String error404(){ return "资源不可用"; }}
为了演示效果, 正常的环境应该是返回码值,由前端对码值进行错误逻辑判断跳转到对应错误页面
自定义登陆页面
创建页面将html文件放在resource文件下面
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>登陆</title></head><body><h1>自定义登陆页面</h1> <form action="/login" method="post"> <span>用户名称</span><input type="text" name="username" /><br> <span>用户密码</span><input type="password" name="password" /><br> <input type="submit" value="登陆"> </form></body></html>
指定登陆请求
import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;/** * login请求 */@Controllerpublic class LoginController { @RequestMapping("/login") public String login(){ return "login"; }}
import org.springframework.context.annotation.Bean;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.crypto.password.NoOpPasswordEncoder;import org.springframework.stereotype.Component;@Component@EnableWebSecuritypublic class SecurityConfig extends WebSecurityConfigurerAdapter { /** * 新增Security * 授权账户 * @param auth * @throws Exception */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { /** * 账号、密码、接口权限 */ auth.inMemoryAuthentication().withUser("mykt-admin").password("mykt-admin").authorities("add","update","get","del"); auth.inMemoryAuthentication().withUser("mykt-add").password("mykt-add").authorities("add"); auth.inMemoryAuthentication().withUser("mykt-update").password("mykt-update").authorities("update"); auth.inMemoryAuthentication().withUser("mykt-get").password("mykt-get").authorities("get"); auth.inMemoryAuthentication().withUser("mykt-del").password("mykt-del").authorities("del"); } /** * 认证方式 * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { /** * 基础认证 */ //http.authorizeRequests().antMatchers("/**").fullyAuthenticated().and().httpBasic(); /** * form表单验证 */ //http.authorizeRequests().antMatchers("/**").fullyAuthenticated().and().formLogin(); /** * 拦截规则 */ http.authorizeRequests().antMatchers("/add").hasAnyAuthority("add") .antMatchers("/get").hasAnyAuthority("get") .antMatchers("/update").hasAnyAuthority("update") .antMatchers("/del").hasAnyAuthority("del") .antMatchers("/login").permitAll() //form验证 .antMatchers("/**").fullyAuthenticated().and().formLogin() //添加自定义跳转页面 .loginPage("/login").and().csrf().disable(); } /** * 加密方式,恢复以前模式 * @return */ @Bean public static NoOpPasswordEncoder PasswordEncoder(){ return (NoOpPasswordEncoder)NoOpPasswordEncoder.getInstance(); }}
这个地方写的时候出现过一个问题,一直报个错误
Hint: This may be the result of an unspecified view, due to default view name generation
后面百度终于查到,说是没加一个包,加了就可以了
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
RBAC权限架构(多对多关联)
用户绑定角色,然后角色绑定权限,那么创建用户的时候,只要给用户选择对应的角色就赋予对应的权限,可以这样理解。
springsecurity整合RBAC权限架构
sql表
/*Navicat MySQL Data TransferSource Server : 127.0.0.1Source Server Version : 50717Source Host : 127.0.0.1:3306Source Database: mayikt_rbacTarget Server Type : MYSQLTarget Server Version : 50717File Encoding : 65001Date: 2021-05-25 04:17:30*/SET FOREIGN_KEY_CHECKS=0;-- ------------------------------ Table structure for sys_permission-- ----------------------------DROP TABLE IF EXISTS `sys_permission`;CREATE TABLE `sys_permission` ( `id` int(10) NOT NULL, `permName` varchar(50) DEFAULT NULL, `permTag` varchar(50) DEFAULT NULL, `url` varchar(255) DEFAULT NULL COMMENT '请求url', PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;-- ------------------------------ Records of sys_permission-- ----------------------------INSERT INTO `sys_permission` VALUES ('1', '查询用户', 'getUser', '/getUser');INSERT INTO `sys_permission` VALUES ('2', '添加用户', 'addUser', '/addUser');INSERT INTO `sys_permission` VALUES ('3', '修改用户', 'updateUser', '/updateUser');INSERT INTO `sys_permission` VALUES ('4', '删除用户', 'delUser', '/delUser');-- ------------------------------ Table structure for sys_role-- ----------------------------DROP TABLE IF EXISTS `sys_role`;CREATE TABLE `sys_role` ( `id` int(10) NOT NULL, `roleName` varchar(50) DEFAULT NULL, `roleDesc` varchar(50) DEFAULT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;-- ------------------------------ Records of sys_role-- ----------------------------INSERT INTO `sys_role` VALUES ('1', 'admin', '管理员');INSERT INTO `sys_role` VALUES ('2', 'add_user', '添加管理员');-- ------------------------------ Table structure for sys_role_permission-- ----------------------------DROP TABLE IF EXISTS `sys_role_permission`;CREATE TABLE `sys_role_permission` ( `role_id` int(10) DEFAULT NULL, `perm_id` int(10) DEFAULT NULL, KEY `FK_Reference_3` (`role_id`), KEY `FK_Reference_4` (`perm_id`), CONSTRAINT `FK_Reference_3` FOREIGN KEY (`role_id`) REFERENCES `sys_role` (`id`), CONSTRAINT `FK_Reference_4` FOREIGN KEY (`perm_id`) REFERENCES `sys_permission` (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;-- ------------------------------ Records of sys_role_permission-- ----------------------------INSERT INTO `sys_role_permission` VALUES ('1', '1');INSERT INTO `sys_role_permission` VALUES ('1', '2');INSERT INTO `sys_role_permission` VALUES ('1', '3');INSERT INTO `sys_role_permission` VALUES ('1', '4');INSERT INTO `sys_role_permission` VALUES ('2', '2');-- ------------------------------ Table structure for sys_user-- ----------------------------DROP TABLE IF EXISTS `sys_user`;CREATE TABLE `sys_user` ( `id` int(10) NOT NULL, `username` varchar(50) DEFAULT NULL, `realname` varchar(50) DEFAULT NULL, `password` varchar(50) DEFAULT NULL, `createDate` date DEFAULT NULL, `lastLoginTime` date DEFAULT NULL, `enabled` int(5) DEFAULT NULL, `accountNonExpired` int(5) DEFAULT NULL, `accountNonLocked` int(5) DEFAULT NULL, `credentialsNonExpired` int(5) DEFAULT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;-- ------------------------------ Records of sys_user-- ----------------------------INSERT INTO `sys_user` VALUES ('1', 'mayikt_admin', '张三', '99743025dc21f56c63d0cb2dd34f06f5', '2018-11-13', '2018-11-13', '1', '1', '1', '1');INSERT INTO `sys_user` VALUES ('2', 'mayikt_add', '小余', 'a5a6996f2e1953161522a93cbb5fb556', '2018-11-13', '2018-11-13', '1', '1', '1', '1');-- ------------------------------ Table structure for sys_user_role-- ----------------------------DROP TABLE IF EXISTS `sys_user_role`;CREATE TABLE `sys_user_role` ( `user_id` int(10) DEFAULT NULL, `role_id` int(10) DEFAULT NULL, KEY `FK_Reference_1` (`user_id`), KEY `FK_Reference_2` (`role_id`), CONSTRAINT `FK_Reference_1` FOREIGN KEY (`user_id`) REFERENCES `sys_user` (`id`), CONSTRAINT `FK_Reference_2` FOREIGN KEY (`role_id`) REFERENCES `sys_role` (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;-- ------------------------------ Records of sys_user_role-- ----------------------------INSERT INTO `sys_user_role` VALUES ('1', '1');INSERT INTO `sys_user_role` VALUES ('2', '2');
然后就是SpringBoot整合mybatis(略)
那么我们现在要做的就是把以前写死的账号、密码、url都要通过查询数据动态的拿到,那么这个就是我们接下来要做的事情。
SecurityConfig
import com.my.mapper.PermissionMapper;import com.my.model.PermissionModel;import com.my.service.serviceimpl.MemberDetailsServiceimpl;import com.my.utils.MD5Util;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.annotation.Bean;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.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;import org.springframework.security.crypto.password.NoOpPasswordEncoder;import org.springframework.security.crypto.password.PasswordEncoder;import org.springframework.stereotype.Component;import java.util.List;@Component@EnableWebSecuritypublic class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private PermissionMapper permissionMapper; @Autowired private MemberDetailsServiceimpl memberDetailsServiceimpl; /** * 新增Security * 授权账户 * @param auth * @throws Exception */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(memberDetailsServiceimpl).passwordEncoder(new PasswordEncoder() { /** * 对用户输入的密码进行加密 * @param charSequence * @return */ @Override public String encode(CharSequence charSequence) { String encode = MD5Util.encode((String) charSequence); return encode; } /** * 加密比对 * @param charSequence 输入明文密码 * @param password 数据库中的密码 * @return true 登陆成功 false 密码错误 */ @Override public boolean matches(CharSequence charSequence, String password) { //输入的密码进行加密 String rawPass = MD5Util.encode((String) charSequence); //对比密码 return password.equals(rawPass); } }); } /** * 认证方式 * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry authorizeRequests = http.authorizeRequests(); //查询到所有的权限 /** * sql * SELECT * id, * permName, * permTag, * url * FROM * sys_permission */ List<PermissionModel> permissionList = permissionMapper.getPermissionList(); permissionList.forEach(x->{ //添加规则 authorizeRequests.antMatchers(x.getUrl()).hasAnyAuthority(x.getPermTag()); }); authorizeRequests .antMatchers("/login").permitAll() .antMatchers("/**").fullyAuthenticated().and().formLogin() //添加自定义跳转页面 .loginPage("/login").and().csrf().disable(); } /** * 加密方式,恢复以前模式 * @return */ @Bean public static NoOpPasswordEncoder PasswordEncoder(){ return (NoOpPasswordEncoder)NoOpPasswordEncoder.getInstance(); }}
重写UserDetailsService.loadUserByUsername方法
import com.my.mapper.PermissionMapper;import com.my.mapper.UserMapper;import com.my.model.PermissionModel;import com.my.model.UserModel;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;/** * 重写UserDetailsService.loadUserByUsername方法 */@Componentpublic class MemberDetailsServiceimpl implements UserDetailsService { @Autowired private UserMapper userMapper; @Autowired private PermissionMapper permissionMapper; @Override public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException { //根据登陆的userName查询这个用户 /** * sql *SELECT * id, * username, * realname, * PASSWORD, * createDate, * lastLoginTime, * enabled, * accountNonExpired, * accountNonLocked, * credentialsNonExpired * FROM * sys_user * where * username=#{username} */ UserModel user = userMapper.getUser(userName); if(user == null){ return null; } //根据用户查询权限 /** * sql * SELECT *s6.id, *s6.permName, *s6.permTag, *s6.url * FROM *( SELECT s2.user_id FROM sys_user s1 LEFT JOIN sys_user_role s2 ON s1.id = s2.user_id WHERE s1.username = #{username} ) s4 *LEFT JOIN sys_role s3 ON s4.user_id = s3.id *LEFT JOIN sys_role_permission s5 ON s5.role_id = s3.id *LEFT JOIN sys_permission s6 ON s6.id = s5.perm_id */ List<PermissionModel> permissionList = permissionMapper.getPermission(user.getUsername()); //创建权限集合 List<GrantedAuthority> grantedAuthorities = new ArrayList<>(); //添加权限 permissionList.forEach(x ->{ grantedAuthorities.add(new SimpleGrantedAuthority(x.getPermTag())); }); //将权限绑定到user user.setAuthorities(grantedAuthorities); return user; }}
UserModel 这个地方要注意,这个User实体类实现了UserDetails重写了方法,那么这些方法默认是返回false,我们要改成true,不然登陆会出现失败的情况。还有一个问题,就是重写UserDetails的方法,刚好我们字段也有一个类似的字段,在启动的时候项目就检查到这个不符合规范,最后我也是将我实体类中的字段set get方法删除。
/** * 用户表 */public class UserModel implements UserDetails { private Integer id; private String username; private String realname; private String password; private Date createDate; private Date lastLoginTime; private Integer enabled; private Integer accountNonExpired; private Integer accountNonLocked; private Integer credentialsNonExpired;//用户跟权限板顶,一对多 private List<GrantedAuthority> authorities = new ArrayList<>(); @Override public List<GrantedAuthority> getAuthorities() { return authorities; } public void setAuthorities(List<GrantedAuthority> authorities) { this.authorities = authorities; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getUsername() { return username; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } public void setUsername(String username) { this.username = username; } public String getRealname() { return realname; } public void setRealname(String realname) { this.realname = realname; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public Date getCreateDate() { return createDate; } public void setCreateDate(Date createDate) { this.createDate = createDate; } public Date getLastLoginTime() { return lastLoginTime; } public void setLastLoginTime(Date lastLoginTime) { this.lastLoginTime = lastLoginTime; }}
MD5加密password
import java.security.MessageDigest;public class MD5Util { //盐 private static final String SALT = "mayikt"; public static String encode(String password) { password = password + SALT; MessageDigest md5 = null; try { md5 = MessageDigest.getInstance("MD5"); } catch (Exception e) { throw new RuntimeException(e); } char[] charArray = password.toCharArray(); byte[] byteArray = new byte[charArray.length]; for (int i = 0; i < charArray.length; i++) byteArray[i] = (byte) charArray[i]; byte[] md5Bytes = md5.digest(byteArray); StringBuffer hexValue = new StringBuffer(); for (int i = 0; i < md5Bytes.length; i++) { int val = ((int) md5Bytes[i]) & 0xff; if (val < 16) { hexValue.append("0"); } hexValue.append(Integer.toHexString(val)); } return hexValue.toString(); }}
演示admin账户,全部权限。我只写了一个查询,其他省略了。
演示mykt-add,这个账户只有add权限,那么我们去用这个账户登陆去查询看看
最后我将代码上传到百度网盘
链接:https://pan.baidu.com/s/1ImSsNg1XdMlZEU922jEiKA
提取码:yyds