引言
我先说明一下,我写这篇文章的原因:因为毕业设计的后台需要使用到Spring Security,但是之前从来没有使用过Security,就直接百度Security如何整合Spring Boot。期间查阅了不下10篇文章,自己编写代码了不下3次,都没有成功,一直卡在读取数据库那里。这些文章基本上全都是代码,看得云里雾里的,基本上没有注释,让没有使用过Spring Security的人如何下手呢。(也有可能是我太菜了,确实。。。)
如果你也是没有使用过Spring Security的话,那么这篇文章将十分适合你!!
代码我已经提交到了gitee上了,一边看着文章一边看着总的代码更有助于理解。
代码的类、变量的命名确实大学问,我是随便命名的,大家不要学我。。。
gitee地址:https://gitee.com/zh_727729853/spring-boot–spring-security
SrpingSecurity简介
SpringSecurity基于Spring框架,提供了一套Web应用安全性的完整解决方案。
主要包括了2个重要区域:“认证” 和 “授权”。
(1)认证:判断某个用户是否能登录。
(2)授权:判断某个用户是否有权限去做某些事情。
优点:
- 和Spring无缝结合
- 全面的权限控制
- 专门为Web开发sheji
缺点:
- 重量级
Shiro
优点:
- 轻量级
缺点:
- 有些特定需求需要手动编写代码
快速入门
创建一个SpringBoot的项目,导入Web和Security的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
这样就已经集成了Security了,我们编写一个Controller测试一下
@RestController
@RequestMapping("/test")
public class TestController {
@GetMapping("/security")
public String test1(){
return "Hello,Security";
}
}
当我们在浏览器输入这个请求地址的时候,默认会跳转到Security的登录界面,提示需要我们登录
默认的账号是:user 密码会在你IDEA的输出控制台上
当我们成功输入了用户名、密码后,就会正常显示网页的内容了。
OK,快速入门就这么简单得结束了!
UserDetailsService接口
当我们什么都没有配置的时候,默认使用的是Security默认的账号和随机生成的密码。我们实际开发中,登录的账号和密码应该从数据库中查询。所以我们要通过自定义逻辑控制认证逻辑。
我们编写一个类实现这个接口,然后重写loadUserByUsername(),在里面实现通过用户名查询数据库,得到这个用户的所有信息,将它返回给Security进行处理。
UserDeatilsService接口:查询数据库用户名、密码。
创建类继承UsernamePasswordAuthenticationFilter,重写3个方法。
创建类实现UserDetailsService接口,编写查询数据库过程,返回User对象。这个User对象是Security提供的对象。
PasswordEncoder接口
主要是用来对密码加密。用于返回User对象的密码加密。
我们一般使用里面的BCrypt。
设置用户名密码的方式
一共有3种方式:
- 通过配置文件(application.yml/properties)
- 通过配置类
- 自定义编写实现类
SpringSecurity查找用户名密码的顺序:
- 首先会在你的配置文件或者配置类中查找,有的话直接使用你配置好的。如果没有才会到你自定义编写的实现类中查找。(上面的三种方式按照1.2.3按顺序执行的)
方式一(通过配置文件):
spring.security.user.name=zh
spring.security.user.password=727729853
方式二:通过配置类
首先,创建一个config包,在config包中创建SecurityConfig类,并且继承WebSecurityConfigurerAdapter。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder(){
//密码加密 使用的是PasswordEncoder中的BCrypt加密
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//密码需要解码,调用encode()
//roles是给这个用户设置的权限(现在还用不上)
auth.inMemoryAuthentication().withUser("zh").password(passwordEncoder().encode("123456")).roles("admin");
}
}
如果配置成功,启动的话,IDEA的控台不会再给你输出它设置好的随机密码
登录localhost:8080/test/security
然后用自己设置好的账号密码登录
方式三:
注意:
要先注释掉application.yml/properties的账号密码
和SecurityConfig类的configure(AuthenticationManagerBuilder auth) 中的 auth.inMemoryAuthentication().withUser("zh").password(passwordEncoder().encode("123456")).roles("admin");
(就是之前方式一和方式二的账号密码)
开始,我是在Service包中创建一个MyUserService类,实现了UserDetailsService接口
//需要设置bean的名称,否则到时候注入的时候,因为有2个UserDetailsService类型的bean,Spring不知道注入哪个。
@Service("userDetailsService")
public class MyUserService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
//我们先写死数据 后面再通过数据库进行查询
//通过Security自带的工具类AuthorityUtils获取权限
//这里只用知道大概意思就好了,会用就OK了
List<GrantedAuthority> authorityList= AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
//这里的User是Security自带的
//里面的参数是 用户名,密码,权限
return new User("zh",new BCryptPasswordEncoder().encode("111111"),authorityList);
}
}
这是Security自带的User对象的参数(源码)
修改一下SecurityConfig类
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//注意这里,注入的是UserDetailsService,不是我们创建的类MyUserService
@Autowired
private UserDetailsService userDetailsService;
@Bean
public PasswordEncoder passwordEncoder(){
//密码加密 使用的是PasswordEncoder中的BCrypt加密
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//设置获取账号密码的方式是通过自定义编写类
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
}
然后我们像之前访问localhost:8080/test/security 然后登录就OJBK了。
用数据库进行认证授权
因为需要连接数据库,所以必定要准备要依赖,我这里使用的是mybatis-plus,可以简化mybatis开发
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
在数据库中建立3张表:
1、用户表 account
2、角色表 role
3、权限表 perssion
1、account
2、 role
3、perssion
pojo 的名称为Account(尽量别用User,因为使用User命名的类太多了,容易导出包)
@AllArgsConstructor
@NoArgsConstructor
@Data
public class Account {
private String id;
private String username;
private String password;
private boolean deleteFlag;
private int roleId;
}
dao 的名称为AccountMapper
不会Mybatis-plus的就自己写SQL语句(Select * from 表名 where username=#{username})
@Repository
public interface AccountMapper extends BaseMapper<Account> {
}
修改MyUserService类
/**
* 主要作用是接收前端传过来的用户名
* 然后自己写业务代码从数据库中查询
*/
//需要设置bean的名称
@Service("userDetailsService")
public class MyUserService implements UserDetailsService {
@Autowired
private AccountMapper accountMapper;
@Autowired
private PerssionMapper perssionMapper;
@Autowoired
private RoleMapper roleMapper;
//方法的形参名称修改一下 改成username
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//mybatis-plus 等价于(Select * from 表名 where username=#{username})
QueryWrapper<Account> queryWrapper = new QueryWrapper();
queryWrapper.eq("username",username);
Account account = accountMapper.selectOne(queryWrapper);
if (account==null){
throw new RuntimeException("没有这个用户");
}
List<GrantedAuthority> authorityList = null;
if (StringUtils.isNotBlank(users.getRoleId())) {
Role role = roleMapper.findRoleById(users.getRoleId());
if (role != null) {
// 获取角色名称
authorityList = AuthorityUtils.commaSeparatedStringToAuthorityList(role.getRoleName());
// 获取角色权限
Set<String> set = perssionMapper.getPerssionById(role.getId());
if (!set.isEmpty()) {
for (String s : set) {
authorityList.add(new SimpleGrantedAuthority(s));
}
}
}
//将查询到的相关信息返回给Security
return new User(users.getUsername(), users.getPassword(), authorityList);
}
return null;
}
}
自定义登录页面
先配置一下SecurityConfig类
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//注意这里,注入的是UserDetailsService,不是我们创建的类MyUserService
@Autowired
private UserDetailsService userDetailsService;
@Bean
public PasswordEncoder passwordEncoder(){
//密码加密 使用的是PasswordEncoder中的BCrypt加密
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//设置获取账号密码的方式是通过自定义编写类
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/login.html") //登录的页面 注意:这里有个"/"
.loginProcessingUrl("/user/login")
//登录提交请求的url 我们不需要在controller中编写这个接口,页面提交的时候Security后帮我们拦截然后去MyUserService中的loadUserByUsername()去数据库中查找
.defaultSuccessUrl("/test/security").permitAll() //登录成功后默认跳转的页面
.and().authorizeRequests().antMatchers("/test/hello","/user/login").permitAll()
//哪些url路径可以直接访问,不需要登录 注意需要放行css、js等资源
.anyRequest().authenticated() //所有请求都可以访问?
.and().csrf().disable(); //关闭csrf
}
}
在Controller中添加一个路径
@RestController
@RequestMapping("/test")
public class TestController {
//测试中,这个接口需要登录才能访问
@GetMapping("/security")
public String test1(){
return "Hello,Security";
}
//可以直接访问
@GetMapping("/hello")
public String hello(){
return "hello";
}
}
登录页面 login.html 在resources/static目录下
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/html">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/user/login" method="post">
用户名:<input name="username" type="text"><br/>
密的码:<input name="password" type="password">
<input type="submit">
</form>
</body>
</html>
然后就可以进行相关测试了
权限认证
介绍一下方法:
-
hasAuthority() 方法里面只能写一个权限角色 只有符合这个权限角色的才能访问
-
hasAnyAuthority() 方法里面可以设置多个权限角色,只要符合其中之一的都可以访问
-
hasRole() 方法里的角色,底层会自动帮你和ROLE_拼接。(例如:方法里的参数是admin,对应需要的权限角色就是ROLE_admin)
hasAnyRole() 也是同理
当没有权限访问的时候,页面会报403错误。
SecurityConfig
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled=true) // 开启后,可以通过注解的形式标识接口有哪些角色或者权限可以访问
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//注意这里,注入的是UserDetailsService,不是我们创建的类MyUserService
@Autowired
private UserDetailsService userDetailsService;
@Bean
public PasswordEncoder passwordEncoder(){
//密码加密 使用的是PasswordEncoder中的BCrypt加密
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//设置获取账号密码的方式是通过自定义编写类
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/login.html") //登录的页面 注意:这里有个"/"
.loginProcessingUrl("/user/login")
//登录提交请求的url 我们不需要在controller中编写这个接口,页面提交的时候Security后帮我们拦截然后去MyUserService中的loadUserByUsername()去数据库中查找
.defaultSuccessUrl("/test/security").permitAll() //登录成功后默认跳转的页面
.and().authorizeRequests().antMatchers("/test/hello","/user/login").permitAll() //哪些url路径可以直接访问,不需要登录
.antMatchers("/test/authen").hasAuthority("admin") //只有admin才能访问
.antMatchers("/test/any").hasAnyAuthority("admin,student") //只有admin或者student权限才能访问
.anyRequest().authenticated() //所有请求都可以访问?
.and().csrf().disable(); //关闭csrf
}
}
TestController新增两个接口
//只有admin才能访问的方法 配置要看SecurityConfig
@GetMapping("/authen")
public String authen(){
return "authen";
}
//admin、student都能访问的方法
@GetMapping("/any")
public String any(){
return "any";
}
自定义403页面
我们新建一个html页面(my403.html)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>没有权限!!!</h1>
</body>
</html>
修改SecurityConfig类的配置 主要加了这一行代码 http.exceptionHandling().accessDeniedPage("/my403.html");
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//注意这里,注入的是UserDetailsService,不是我们创建的类MyUserService
@Autowired
private UserDetailsService userDetailsService;
@Bean
public PasswordEncoder passwordEncoder(){
//密码加密 使用的是PasswordEncoder中的BCrypt加密
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//设置获取账号密码的方式是通过自定义编写类
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/login.html") //登录的页面 注意:这里有个"/"
.loginProcessingUrl("/user/login")
//登录提交请求的url 我们不需要在controller中编写这个接口,页面提交的时候Security后帮我们拦截然后去MyUserService中的loadUserByUsername()去数据库中查找
.defaultSuccessUrl("/test/security").permitAll() //登录成功后默认跳转的页面
.and().authorizeRequests().antMatchers("/test/hello","/user/login").permitAll() //哪些url路径可以直接访问,不需要登录
.antMatchers("/test/authen").hasAuthority("admin") //只有admin才能访问
.antMatchers("/test/any").hasAnyAuthority("admin,student") //只有admin或者student权限才能访问
.anyRequest().authenticated() //所有请求都可以访问?
.and().csrf().disable(); //关闭csrf
//没有权限的时候,跳转到我们自定义的403页面
http.exceptionHandling().accessDeniedPage("/my403.html");
}
}
我们再修改一下MyUserService中,我们自己写死的权限,随便修改成别的就好了!!
登录测试
注解快速开发
说明:三个注解都要开启注解支持 (大概这个意思吧~~)
- @Secured() 作用于方法之上,表名拥有哪些权限角色才能访问该方法。
注意:@Secured ()里的角色要以ROLE_ 开头,登录用户的角色权限也要以ROLE_ 开头。
- @PreAuthorize() 在执行方法之前进行权限校验。
使用方法:@PreAuthorize(“hasAnyAuthority(‘角色权限’)”) 注意符号的使用 角色权限直接填写 不需要加ROLE_。
- @PostAuthorize() 执行完方法之后进行权限校验,失败则向用户返回权限不足,但是里面的方法还是执行了。(使用的不多)
使用方法:@PostAuthorize(“hasAnyAuthority(‘角色权限’)”) 注意符号的使用 角色权限直接填写 不需要加ROLE_。
@Secured使用
首先,要开始Security的注解扫描 @EnableGlobalMethodSecurity(securedEnabled = true) 可以放在Config类上也可以是启动类上
@SpringBootApplication
@MapperScan("com.zhong.dao")
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
//开启@Secured支持securedEnabled 开始@PreAuthorize和@PostAuthorize支持 都是prePostEnabled
public class SpringBootAndSecurity {
public static void main(String[] args) {
SpringApplication.run(SpringBootAndSecurity.class,args);
}
}
新建一个Test2Controller 这个类里所有的接口都是使用注解来标识权限的。
@RestController
@RequestMapping("/test2")
public class Test2Controller {
@GetMapping("/and")
@Secured({"ROLE_admin","ROLE_admin1"}) //必须是以ROLE_开头 用户的角色也要是以ROLE_开头
public String test1(){
return "使用注解进行权限校验成功";
}
}
@PreAuthorize使用
在Test2Controller中添加接口
//执行方法之前进行校验
@GetMapping("/before")
@PreAuthorize("hasAnyAuthority('student')")
public String before(){
return "before";
}
测试。。。。
@PostAuthorize使用
在Test2Controller中添加接口
//执行完方法之后进行权限校验,失败则向用户返回权限不足,但是里面的方法还是执行了。
@GetMapping("/after")
@PostAuthorize("hasAnyAuthority('student')")
public String after(){
return "after";
}
用户注销功能
编写一个Handler
@Component
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.sendRedirect("/login.html");
}
}
修改WebSecurityConfig类
@Resource
private MyLogoutSuccessHandler myLogoutSuccessHandler;
// 省略很多代码
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
//自定义登录的页面
.loginPage("/login.html")
//登录的请求url地址
.loginProcessingUrl("/user/login")
//登录成功后跳转的默认地址
.defaultSuccessUrl("/admin/test").permitAll()
.and().authorizeRequests()
//放行的资源(不需要登陆)
.antMatchers
("/admin/login", "/", "/css/**", "/fonts/**", "/js/**", "/img/**", "/plugins/**", "/index/**", "/order/**", "/catrgory/**", "/goods/**", "/test/**", "/paysuccess.html").permitAll()
.anyRequest().authenticated()
// 用户退出 新增一行代码
.and().logout().logoutSuccessHandler(myLogoutSuccessHandler)
.and().csrf().disable(); // 自定义成功页面的时候,要使用csrf().disable() 关闭这个过滤器
}
最后编写一个退出按钮,请求路径是localhost:端口/logout即可