Spring Security项目搭建
Spring-Security由spring提供的安全框架,基于url实现,这一点和shiro类似,在ssm项目中配置比较繁琐,一般它用在springboot和springcloud项目中
1. 准备一个web项目
添加测试接口
并测试项目没有问题后添加maven节点
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
2. 配置springsecurity
添加springsecurity的配置文件SecurityConfig.java,并继承WebSecurityConfigurerAdapter
先看看WebSecurityConfigurerAdapter的方法
方法有很多重点是红框框起来的三个方法,我们需要重写他们
l 第一个方法:
/** * 配置用户 * * @param auth * @throws Exception */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { super.configure(auth); }
从入参不难看出,这个方法是对用户进行配置的,也就是说你需要对什么样的用户进行安全管理,是基于内存还是数据库?都可以在这里配置
l 第二个方法:
/** * 配置web的 * * @param web+ * @throws Exception */ @Override public void configure(WebSecurity web) throws Exception { super.configure(web); }
这个方法同样不难发现是基于web的配置
l 第三个方法:
/** * 配置安全 * * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { super.configure(http); }
这个就是安全相关的配置
下边简单写一个配置来看看效果
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { /** * 密码比对器 * * @return */ @Bean PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } /** * 配置用户 * * @param auth * @throws Exception */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication()//基于内存的用户和密码 .withUser("lhf").password(passwordEncoder().encode("123456")).roles("admin"); } /** * 配置web的 * * @param web+ * @throws Exception */ @Override public void configure(WebSecurity web) throws Exception { super.configure(web); } /** * 配置安全 * * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().anyRequest().authenticated()//所有请求都必须登录 .and() .formLogin()//开启表单登录 .permitAll() .and() .csrf().disable()//关闭防csrf攻击 .cors() ; } }
这时请求http://localhost:8080/hello 会被强行拦截到springsecurity的内置的登录页面,然后输入用户名和密码进行登录即可。
一个简单的登录就告一段落,下边一滴一滴的开始配置他。
3. 准备数据库
众所周知的,权限这一块的数据库是一般分为5个表:用户、角色、用户角色、权限、角色权限。(严格来说应该是是哪个表,但是他们之间有事多对多关系所以说多了用户角色表,和角色权限表,两个中间表)
用户表:
CREATE TABLE `sys_user` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键', `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '密码', `real_name` varchar(20) NOT NULL COMMENT '真实姓名', `email` varchar(50) DEFAULT NULL COMMENT '电子邮件', `head_img` varchar(255) DEFAULT NULL COMMENT '头像', `phone` varchar(11) DEFAULT NULL COMMENT '联系方式', `is_lock` int(1) DEFAULT '0' COMMENT '是否锁定 0未锁定 1已锁定', `is_del` int(1) DEFAULT '0' COMMENT '0未删除 1已删除', `create_time` datetime DEFAULT NULL COMMENT '创建时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=46 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
角色表:
CREATE TABLE `sys_role` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键', `role_name` varchar(10) NOT NULL COMMENT '角色名', `role` varchar(20) NOT NULL COMMENT '角色', `role_ico` varchar(50) DEFAULT NULL COMMENT '角色图标', `is_del` int(1) DEFAULT '0' COMMENT '0未删除 1已删除', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
用户角色关联表:
CREATE TABLE `sys_user_role` ( `user_id` int(11) NOT NULL COMMENT '用户id', `role_id` int(11) NOT NULL COMMENT '角色id', UNIQUE KEY `uid_rid_index` (`user_id`,`role_id`) USING BTREE, KEY `user_role_rid` (`role_id`), CONSTRAINT `user_role_rid` FOREIGN KEY (`role_id`) REFERENCES `sys_role` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT `user_role_uid` FOREIGN KEY (`user_id`) REFERENCES `sys_user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
权限表:
CREATE TABLE `sys_per` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键', `per_name` varchar(10) NOT NULL COMMENT '权限名字', `per` varchar(20) NOT NULL COMMENT '权限', `per_ico` varchar(50) DEFAULT NULL COMMENT '权限图标', `compoment` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '组件名字', `path` varchar(50) DEFAULT NULL COMMENT '请求路径', `type` int(1) DEFAULT NULL COMMENT '0 菜单 1按钮', `parent_id` int(11) NOT NULL DEFAULT '0' COMMENT '父类id', `is_del` int(1) DEFAULT '0' COMMENT '0未删除 1已删除', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
角色权限关联表:
CREATE TABLE `sys_role_per` ( `role_id` int(11) NOT NULL COMMENT '角色id', `per_id` int(11) NOT NULL COMMENT '权限id', UNIQUE KEY `rid_pid_index` (`role_id`,`per_id`) USING BTREE, KEY `role_per_pid` (`per_id`), CONSTRAINT `role_per_pid` FOREIGN KEY (`per_id`) REFERENCES `sys_per` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT `role_per_rid` FOREIGN KEY (`role_id`) REFERENCES `sys_role` (`id`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
这样数据库就准备好了(后边就是正常的springboot+mybatis的配置省略)
4. UserDetails
在springsecurity中userdetails是一个重要的对象,他是一个接口,提供用户的信息(包括用户名、密码、权限等).框架提供了默认的实现User,但是往往他是不能满足我们的需求,或者说不方便,于是可以自定义一个实现
package com.lhf.springsecurity.entity;
import lombok.Data; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; import java.util.Date; import java.io.Serializable; import java.util.List; import java.util.stream.Collectors; /** * (SysUser)实体类 * * @author 刘会发 * @since 2020-05-03 09:28:53 */ @Data public class SysUser implements Serializable, UserDetails { private static final long serialVersionUID = -13438204047342005L; /** * 主键 */ private Integer uid; /** * 用户名 */ private String username; /** * 密码 */ private String password; /** * 真实姓名 */ private String realName; /** * 电子邮件 */ private String email; /** * 头像 */ private String headImg; /** * 联系方式 */ private String phone; /** * 是否锁定 0未锁定 1已锁定 */ private Integer isLock; /** * 0未删除 1已删除 */ private Integer isDel; /** * 创建时间 */ private Date createTime; private List<SysRole> roles; @Override public Collection<? extends GrantedAuthority> getAuthorities() { return roles.stream().flatMap(item -> item.getPers().stream().map(per -> new SimpleGrantedAuthority(per.getPer()))).collect(Collectors.toSet()); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return this.isLock == 0; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }
其中GrantedAuthority 是保存用户角色信息的对象,如果需要用权限控制的话可以修改这里的实现方式。
5. UserDetailsService
UserDetailsService 是一个接口,他是提供用户信息的接口,他的返回值就是一个UserDetails
所以代码这样写:
package com.lhf.springsecurity.service; import com.lhf.springsecurity.entity.SysUser; import org.springframework.security.core.userdetails.UserDetailsService; import java.util.List; /** * (SysUser)表服务接口 * * @author 刘会发 * @since 2020-05-03 09:28:53 */ public interface SysUserService extends UserDetailsService { } 实现类: package com.lhf.springsecurity.service.impl; import com.lhf.springsecurity.entity.SysUser; import com.lhf.springsecurity.dao.SysUserDao; import com.lhf.springsecurity.service.SysUserService; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.util.List; /** * (SysUser)表服务实现类 * * @author 刘会发 * @since 2020-05-03 09:28:53 */ @Service("sysUserService") public class SysUserServiceImpl implements SysUserService { @Resource private SysUserDao sysUserDao; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { SysUser user = sysUserDao.login(username); if (user == null) throw new UsernameNotFoundException("用户未找到"); return user; } } 通过数据库查询出SysUser对象(前边实现UserDetails),并将他返回。 到这里用户信息提供者也配置好了,剩下的只需要简单配置springsecurity即可 /** * 配置用户 * * @param auth * @throws Exception */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userService).passwordEncoder(passwordEncoder());//配置用户提供者,并配置密码比对器 }
现在请求http://localhost:8080/hello 并登陆
6. 权限控制
l 代码配置
主要是在SecurityConfig中进行配置
/** * 安全配置 * * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/hello").hasAuthority("ADMIN") .antMatchers("/look").hasAuthority("LOOK") .anyRequest().authenticated() .and() .formLogin() .permitAll() .and() .csrf().disable() .cors().disable(); }
.antMatchers("/hello").hasAuthority("ADMIN"): 拥有ADMIN角色才能访问 /hello接口
这时登陆后分别访问http://localhost:8080/look;http://localhost:8080/hello
/look 接口将不会返回任何信息,还会报错
SpringSecurity通过url控制权限,自然和shiro一样,他们是有先后顺序的如果这样
/** * 安全配置 * * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/**").permitAll()//所有接口给予任何权限 .antMatchers("/hello").hasAuthority("PER_SYSTEM") .antMatchers("/look").hasAuthority("LOOK") .anyRequest().authenticated() .and() .formLogin() .permitAll() .and() .csrf().disable() .cors().disable(); }
在最前边给了所有路径任何权限,那么下边的配置将不会生效
.antMatchers("/**").permitAll(); 给任何请求路径所有的权限
l 注解配置
自然springsecurity也是支持注解配置的但是需要开启自动配置
@EnableGlobalMethodSecurity(prePostEnabled = true)
可以把它添加到SecurityConfig上,也可以添加到启动类上
添加权限注解
/** * <p></p> * * @author zy 刘会发 * @version 1.0 * @since 2020/5/3 */ @RestController public class TestController { @PreAuthorize("hasAnyAuthority('PER_SYSTEM')") @RequestMapping("/hello") public String hello() { return "hello"; } @RequestMapping("look") @PreAuthorize("hasAnyAuthority('LOOK')") public String look() { return "look"; } }
这样在登录admin,分别访问两个接口
7. 自定义登录页面
当然,spring官方给的登录页面略微丑陋,接下来看看如何自定义登录页面
/** * 安全配置 * * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest().authenticated() .and() .formLogin() .loginPage("/login.html")//自定义登录页面的url .loginProcessingUrl("/login")//登录请求的url .permitAll() .and() .csrf().disable() .cors().disable(); }
这里我自己准备了一个登录页面放在了static目录下
页面的表单:
<div class="form"> <div> <span>登录系统</span> <form id="form" action="/login" method="post"> <label> 用户名: <input name="username" type="text"/> </label> <label> 密 码: <input name="password" type="password"> </label> <input type="submit" class="submit" value="登录"/> </form> </div> </div>
重新启动项目,随便请求一个接口会跳转到该页面
8. 自定义登录返回
l 自定义登录成功的返回:
实现AuthenticationSuccessHandler接口,并重写onAuthenticationSuccess方法
public class LoginSuccessHandler implements AuthenticationSuccessHandler { @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { request.setCharacterEncoding("utf-8"); response.setCharacterEncoding("utf-8"); PrintWriter writer = response.getWriter(); //authentication 存放的是该用户的所有信息 writer.write("hello,you are success:"+authentication.getName()); writer.flush(); writer.close(); } }
将他添加到springsecurity中:
/** * 安全配置 * * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest().authenticated() .and() .formLogin() .loginPage("/login.html")//自定义登录页面的url .loginProcessingUrl("/login")//登录请求的url .successHandler(new LoginSuccessHandler())//添加自定义登陆成功返回 .permitAll() .and() .csrf().disable() .cors().disable(); }
这里直接返回一句话,用postman进行测试:
l 自定义登录失败的返回
实现AuthenticationFailureHandler接口,重写onAuthenticationFailure
public class LoginFailureHandler implements AuthenticationFailureHandler { @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException { request.setCharacterEncoding("utf-8"); response.setCharacterEncoding("utf-8"); PrintWriter writer = response.getWriter(); writer.write("hello,you are failure:" + e.getMessage()); writer.flush(); writer.close(); } }
l 自定义未授权返回
实现AccessDeniedHandler接口,重写handle方法
l 自定义未登录时返回
实现AuthenticationEntryPoint接口,重写commence方法
/** * 安全配置 * * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest().authenticated() .and() .formLogin() .loginPage("/login.html")//自定义登录页面的url .loginProcessingUrl("/login")//登录请求的url .successHandler(new LoginSuccessHandler())//添加自定义登陆成功返回 // .successForwardUrl("/index.html")//登录成功转发url .failureHandler(new LoginFailureHandler())//添加自定义登录失败页面 .permitAll() .and() .exceptionHandling() .accessDeniedHandler((request, response, e) -> { // 自定义无权访问返回 request.setCharacterEncoding("utf-8"); response.setCharacterEncoding("utf-8"); PrintWriter writer = response.getWriter(); writer.write("hello,you don't have permission"); writer.flush(); writer.close(); }) .authenticationEntryPoint((request, response, e) -> { // 自定义未登录时返回 request.setCharacterEncoding("utf-8"); response.setCharacterEncoding("utf-8"); PrintWriter writer = response.getWriter(); writer.write("hello,you need login"); writer.flush(); writer.close(); }) .and() .csrf().disable() .cors().disable(); }
熟悉jdk1.8的都知道,一个接口有且只有一个方法的时候,可以使用lamda表达式,上诉代码中自定义未授权返回和自定义未登录时返回采用lamda表达式,当然登陆成功和登录失败也同样可以这样(不建议,项目比较大,而自定义返回逻辑复杂时,这个配置类将会十分的臃肿,这里只是为了方便)。
9. 记住我
springsecurity原生支持记住我的功能,有基于内存的、还有基于数据库的
/** * 记住我功能,通过数据库 * * @return */ @Bean PersistentTokenRepository persistentTokenRepository() { JdbcTokenRepositoryImpl impl = new JdbcTokenRepositoryImpl(); impl.setDataSource(dataSource); impl.afterPropertiesSet(); // impl.setCreateTableOnStartup(true); //第一次启动开启,将会创建表,之后直接关闭即可 return impl; }
这里配置一个基于数据库的实现
impl.setCreateTableOnStartup(true) 可以生成默认表结构,在第一次启动时将参数设置为true,以后将不再需要,注释即可
项目地址:
https://github.com/Liuhuifa/spring-security
Sql文件在项目resources目录下
Sql文件中有一些没用的表,自行忽略
Spring Security项目搭建
Spring-Security由spring提供的安全框架,基于url实现,这一点和shiro类似,在ssm项目中配置比较繁琐,一般它用在springboot和springcloud项目中
1. 准备一个web项目
添加测试接口
并测试项目没有问题后添加maven节点
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2. 配置springsecurity
添加springsecurity的配置文件SecurityConfig.java,并继承WebSecurityConfigurerAdapter
先看看WebSecurityConfigurerAdapter的方法
方法有很多重点是红框框起来的三个方法,我们需要重写他们
l 第一个方法:
/**
* 配置用户
*
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
super.configure(auth);
}
从入参不难看出,这个方法是对用户进行配置的,也就是说你需要对什么样的用户进行安全管理,是基于内存还是数据库?都可以在这里配置
l 第二个方法:
/**
* 配置web的
*
* @param web+
* @throws Exception
*/
@Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
}
这个方法同样不难发现是基于web的配置
l 第三个方法:
/**
* 配置安全
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
}
这个就是安全相关的配置
下边简单写一个配置来看看效果
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 密码比对器
*
* @return
*/
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 配置用户
*
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()//基于内存的用户和密码
.withUser("lhf").password(passwordEncoder().encode("123456")).roles("admin");
}
/**
* 配置web的
*
* @param web+
* @throws Exception
*/
@Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
}
/**
* 配置安全
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated()//所有请求都必须登录
.and()
.formLogin()//开启表单登录
.permitAll()
.and()
.csrf().disable()//关闭防csrf攻击
.cors()
;
}
}
这时请求http://localhost:8080/hello 会被强行拦截到springsecurity的内置的登录页面,然后输入用户名和密码进行登录即可。
一个简单的登录就告一段落,下边一滴一滴的开始配置他。
3. 准备数据库
众所周知的,权限这一块的数据库是一般分为5个表:用户、角色、用户角色、权限、角色权限。(严格来说应该是是哪个表,但是他们之间有事多对多关系所以说多了用户角色表,和角色权限表,两个中间表)
用户表:
CREATE TABLE `sys_user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`username` varchar(50) NOT NULL COMMENT '用户名',
`password` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '密码',
`real_name` varchar(20) NOT NULL COMMENT '真实姓名',
`email` varchar(50) DEFAULT NULL COMMENT '电子邮件',
`head_img` varchar(255) DEFAULT NULL COMMENT '头像',
`phone` varchar(11) DEFAULT NULL COMMENT '联系方式',
`is_lock` int(1) DEFAULT '0' COMMENT '是否锁定 0未锁定 1已锁定',
`is_del` int(1) DEFAULT '0' COMMENT '0未删除 1已删除',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=46 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
角色表:
CREATE TABLE `sys_role` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`role_name` varchar(10) NOT NULL COMMENT '角色名',
`role` varchar(20) NOT NULL COMMENT '角色',
`role_ico` varchar(50) DEFAULT NULL COMMENT '角色图标',
`is_del` int(1) DEFAULT '0' COMMENT '0未删除 1已删除',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
用户角色关联表:
CREATE TABLE `sys_user_role` (
`user_id` int(11) NOT NULL COMMENT '用户id',
`role_id` int(11) NOT NULL COMMENT '角色id',
UNIQUE KEY `uid_rid_index` (`user_id`,`role_id`) USING BTREE,
KEY `user_role_rid` (`role_id`),
CONSTRAINT `user_role_rid` FOREIGN KEY (`role_id`) REFERENCES `sys_role` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `user_role_uid` FOREIGN KEY (`user_id`) REFERENCES `sys_user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
权限表:
CREATE TABLE `sys_per` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`per_name` varchar(10) NOT NULL COMMENT '权限名字',
`per` varchar(20) NOT NULL COMMENT '权限',
`per_ico` varchar(50) DEFAULT NULL COMMENT '权限图标',
`compoment` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '组件名字',
`path` varchar(50) DEFAULT NULL COMMENT '请求路径',
`type` int(1) DEFAULT NULL COMMENT '0 菜单 1按钮',
`parent_id` int(11) NOT NULL DEFAULT '0' COMMENT '父类id',
`is_del` int(1) DEFAULT '0' COMMENT '0未删除 1已删除',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
角色权限关联表:
CREATE TABLE `sys_role_per` (
`role_id` int(11) NOT NULL COMMENT '角色id',
`per_id` int(11) NOT NULL COMMENT '权限id',
UNIQUE KEY `rid_pid_index` (`role_id`,`per_id`) USING BTREE,
KEY `role_per_pid` (`per_id`),
CONSTRAINT `role_per_pid` FOREIGN KEY (`per_id`) REFERENCES `sys_per` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `role_per_rid` FOREIGN KEY (`role_id`) REFERENCES `sys_role` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
这样数据库就准备好了(后边就是正常的springboot+mybatis的配置省略)
4. UserDetails
在springsecurity中userdetails是一个重要的对象,他是一个接口,提供用户的信息(包括用户名、密码、权限等).框架提供了默认的实现User,但是往往他是不能满足我们的需求,或者说不方便,于是可以自定义一个实现
package com.lhf.springsecurity.entity;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.Date;
import java.io.Serializable;
import java.util.List;
import java.util.stream.Collectors;
/**
* (SysUser)实体类
*
* @author 刘会发
* @since 2020-05-03 09:28:53
*/
@Data
public class SysUser implements Serializable, UserDetails {
private static final long serialVersionUID = -13438204047342005L;
/**
* 主键
*/
private Integer uid;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 真实姓名
*/
private String realName;
/**
* 电子邮件
*/
private String email;
/**
* 头像
*/
private String headImg;
/**
* 联系方式
*/
private String phone;
/**
* 是否锁定 0未锁定 1已锁定
*/
private Integer isLock;
/**
* 0未删除 1已删除
*/
private Integer isDel;
/**
* 创建时间
*/
private Date createTime;
private List<SysRole> roles;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return roles.stream().flatMap(item -> item.getPers().stream().map(per -> new SimpleGrantedAuthority(per.getPer()))).collect(Collectors.toSet());
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return this.isLock == 0;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
其中GrantedAuthority 是保存用户角色信息的对象,如果需要用权限控制的话可以修改这里的实现方式。
5. UserDetailsService
UserDetailsService 是一个接口,他是提供用户信息的接口,他的返回值就是一个UserDetails
所以代码这样写:
package com.lhf.springsecurity.service;
import com.lhf.springsecurity.entity.SysUser;
import org.springframework.security.core.userdetails.UserDetailsService;
import java.util.List;
/**
* (SysUser)表服务接口
*
* @author 刘会发
* @since 2020-05-03 09:28:53
*/
public interface SysUserService extends UserDetailsService {
}
实现类:
package com.lhf.springsecurity.service.impl;
import com.lhf.springsecurity.entity.SysUser;
import com.lhf.springsecurity.dao.SysUserDao;
import com.lhf.springsecurity.service.SysUserService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
/**
* (SysUser)表服务实现类
*
* @author 刘会发
* @since 2020-05-03 09:28:53
*/
@Service("sysUserService")
public class SysUserServiceImpl implements SysUserService {
@Resource
private SysUserDao sysUserDao;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser user = sysUserDao.login(username);
if (user == null)
throw new UsernameNotFoundException("用户未找到");
return user;
}
}
通过数据库查询出SysUser对象(前边实现UserDetails),并将他返回。
到这里用户信息提供者也配置好了,剩下的只需要简单配置springsecurity即可
/**
* 配置用户
*
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(passwordEncoder());//配置用户提供者,并配置密码比对器
}
现在请求http://localhost:8080/hello 并登陆
6. 权限控制
l 代码配置
主要是在SecurityConfig中进行配置
/**
* 安全配置
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/hello").hasAuthority("ADMIN")
.antMatchers("/look").hasAuthority("LOOK")
.anyRequest().authenticated()
.and()
.formLogin()
.permitAll()
.and()
.csrf().disable()
.cors().disable();
}
.antMatchers("/hello").hasAuthority("ADMIN"): 拥有ADMIN角色才能访问 /hello接口
这时登陆后分别访问http://localhost:8080/look;http://localhost:8080/hello
/look 接口将不会返回任何信息,还会报错
SpringSecurity通过url控制权限,自然和shiro一样,他们是有先后顺序的如果这样
/**
* 安全配置
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/**").permitAll()//所有接口给予任何权限
.antMatchers("/hello").hasAuthority("PER_SYSTEM")
.antMatchers("/look").hasAuthority("LOOK")
.anyRequest().authenticated()
.and()
.formLogin()
.permitAll()
.and()
.csrf().disable()
.cors().disable();
}
在最前边给了所有路径任何权限,那么下边的配置将不会生效
.antMatchers("/**").permitAll(); 给任何请求路径所有的权限
l 注解配置
自然springsecurity也是支持注解配置的但是需要开启自动配置
@EnableGlobalMethodSecurity(prePostEnabled = true)
可以把它添加到SecurityConfig上,也可以添加到启动类上
添加权限注解
/**
* <p></p>
*
* @author zy 刘会发
* @version 1.0
* @since 2020/5/3
*/
@RestController
public class TestController {
@PreAuthorize("hasAnyAuthority('PER_SYSTEM')")
@RequestMapping("/hello")
public String hello() {
return "hello";
}
@RequestMapping("look")
@PreAuthorize("hasAnyAuthority('LOOK')")
public String look() {
return "look";
}
}
这样在登录admin,分别访问两个接口
7. 自定义登录页面
当然,spring官方给的登录页面略微丑陋,接下来看看如何自定义登录页面
/**
* 安全配置
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login.html")//自定义登录页面的url
.loginProcessingUrl("/login")//登录请求的url
.permitAll()
.and()
.csrf().disable()
.cors().disable();
}
这里我自己准备了一个登录页面放在了static目录下
页面的表单:
<div class="form">
<div>
<span>登录系统</span>
<form id="form" action="/login" method="post">
<label>
用户名:
<input name="username" type="text"/>
</label>
<label>
密 码:
<input name="password" type="password">
</label>
<input type="submit" class="submit" value="登录"/>
</form>
</div>
</div>
重新启动项目,随便请求一个接口会跳转到该页面
8. 自定义登录返回
l 自定义登录成功的返回:
实现AuthenticationSuccessHandler接口,并重写onAuthenticationSuccess方法
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
request.setCharacterEncoding("utf-8");
response.setCharacterEncoding("utf-8");
PrintWriter writer = response.getWriter();
//authentication 存放的是该用户的所有信息
writer.write("hello,you are success:"+authentication.getName());
writer.flush();
writer.close();
}
}
将他添加到springsecurity中:
/**
* 安全配置
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login.html")//自定义登录页面的url
.loginProcessingUrl("/login")//登录请求的url
.successHandler(new LoginSuccessHandler())//添加自定义登陆成功返回
.permitAll()
.and()
.csrf().disable()
.cors().disable();
}
这里直接返回一句话,用postman进行测试:
l 自定义登录失败的返回
实现AuthenticationFailureHandler接口,重写onAuthenticationFailure
public class LoginFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
request.setCharacterEncoding("utf-8");
response.setCharacterEncoding("utf-8");
PrintWriter writer = response.getWriter();
writer.write("hello,you are failure:" + e.getMessage());
writer.flush();
writer.close();
}
}
l 自定义未授权返回
实现AccessDeniedHandler接口,重写handle方法
l 自定义未登录时返回
实现AuthenticationEntryPoint接口,重写commence方法
/**
* 安全配置
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login.html")//自定义登录页面的url
.loginProcessingUrl("/login")//登录请求的url
.successHandler(new LoginSuccessHandler())//添加自定义登陆成功返回
// .successForwardUrl("/index.html")//登录成功转发url
.failureHandler(new LoginFailureHandler())//添加自定义登录失败页面
.permitAll()
.and()
.exceptionHandling()
.accessDeniedHandler((request, response, e) -> {
// 自定义无权访问返回
request.setCharacterEncoding("utf-8");
response.setCharacterEncoding("utf-8");
PrintWriter writer = response.getWriter();
writer.write("hello,you don't have permission");
writer.flush();
writer.close();
})
.authenticationEntryPoint((request, response, e) -> {
// 自定义未登录时返回
request.setCharacterEncoding("utf-8");
response.setCharacterEncoding("utf-8");
PrintWriter writer = response.getWriter();
writer.write("hello,you need login");
writer.flush();
writer.close();
})
.and()
.csrf().disable()
.cors().disable();
}
熟悉jdk1.8的都知道,一个接口有且只有一个方法的时候,可以使用lamda表达式,上诉代码中自定义未授权返回和自定义未登录时返回采用lamda表达式,当然登陆成功和登录失败也同样可以这样(不建议,项目比较大,而自定义返回逻辑复杂时,这个配置类将会十分的臃肿,这里只是为了方便)。
9. 记住我
springsecurity原生支持记住我的功能,有基于内存的、还有基于数据库的
/**
* 记住我功能,通过数据库
*
* @return
*/
@Bean
PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl impl = new JdbcTokenRepositoryImpl();
impl.setDataSource(dataSource);
impl.afterPropertiesSet();
// impl.setCreateTableOnStartup(true); //第一次启动开启,将会创建表,之后直接关闭即可
return impl;
}
这里配置一个基于数据库的实现
impl.setCreateTableOnStartup(true) 可以生成默认表结构,在第一次启动时将参数设置为true,以后将不再需要,注释即可
项目地址:
https://github.com/Liuhuifa/spring-security
Sql文件在项目resources目录下
Sql文件中有一些没用的表,自行忽略