Spring Security是Spring提供的安全组件,主要用于在项目中对用户的身份进行识别和认证。
Spring安全框架就是Spring-Security,功能是管理当前项目的用户登录和登录后的权限管理,是Spring框架提供的权限管理和安全方案
和Spring-Security框架功能类似的框架还有Shiro
使用Spring Security之前,需要添加依赖,可以在创建SpringBoot项目时直接勾选,也可以在已经创建好的项目中添加:
org.springframework.boot
spring-boot-starter-security
添加依赖之后,启动服务时,我们会看到一串随机生成的密码,这个密码可以用于登录Spring-Security的系统
用户名:user 密码为启动服务时生成的字符串
一旦加载这个依赖,那么要访问当前项目的资源就必须先登录Spring-Security
如果我们想自定义用户名和密码
那么就需要在application.properties文件中指定
代码如下
spring.security.user.name=admin
spring.security.user.password=666666
有上述配置,启动服务时就不会再生成随机密码,使用配置的用户名和密码即可登录
实际开发中,密码不可能使用明文保存.明文密码可以根据一定的加密算法加密为一个密文密码提高安全性,现在流行的加密算法有md5,Bcrypt等
我们项目中使用SpringSecurity自带的Bcrypt加密,下面我们就来实现一下使用Bcrypt对明文密码进行加密和验证的操作
首先来注入加密对象到Spring容器,在项目中创建一个security包,包中创建SecurityConfig类
// @Configuration表示当前这个类也是Spring的配置类
// Spring扫描到这类时,就会把它当做配置类解析
@Configuration
public class SecurityConfig {
//向spring容器中注入一个加密对象,用于对密码加密
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
测试:
@Autowired
PasswordEncoder passwordEncoder;
//加密
@Test
public void encode(){
String pwd=passwordEncoder.encode("666666");
System.out.println(pwd);
//$2a$10$ytxmGeWcRZObqoDmlhnWxe6KqUjb9DTONmQVKkmwneHQtZw4LQtiq
}
//验证
@Test
public void decode(){
boolean bool=passwordEncoder.matches("666666",
"$2a$10$ytxmGeWcRZObqoDmlhnWxe6KqUjb9DTONmQVKkmwneHQtZw4LQtiq");
System.out.println(bool);
}
一旦项目注入了PasswordEncoder类型的对象,SpringSecurity框架就自动会使用这个对象对当前的明文密码进行加密,将application.properties文件中的用户的密码要修改为加密之后的了
application.properties登录配置修改如下
spring.security.user.name=admin
spring.security.user.password=$2a$10$ytxmGeWcRZObqoDmlhnWxe6KqUjb9DTONmQVKkmwneHQtZw4LQtiq
需要密码加密的时候还需要在配置类中添加PasswordEncoder对象的注入很麻烦
我们可以在密文密码添加特定的算法idSpringSecurity就会自动按加密算法加密了,更简单
具体做法如下application.properties修改
spring.security.user.name=admin
spring.security.user.password={bcrypt}$2a$10$ytxmGeWcRZObqoDmlhnWxe6KqUjb9DTONmQVKkmwneHQtZw4LQtiq
这样之前在SecurityConfig类中注入的PasswordEncoder就可以注释或删除了
@Configuration
public class SecurityConfig {
//向spring容器中注入一个加密对象,用于对密码加密
/*@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}*/
}
在项目有很多用户,都是保存在数据库中的,不可能依靠配置文件的配置进行登录,所以我们要学习怎么在java代码中进行登录的验证,这一系列验证操作要编写在一个java的配置类中
正好使用我们上面章节中创建的SecurityConfig即可!!!
代码实例:
@Configuration
//下面的注解表示开启Spring-Security的权限管理功能
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//配置登录验证(即用户名和密码的验证)
@Override
protected void configure(AuthenticationManagerBuilder auth)
throws Exception {
auth.inMemoryAuthentication().withUser("tom")
.password("{bcrypt}$2a$10$ytxmGeWcRZObqoDmlhnWxe6KqUjb9DTONmQVKkmwneHQtZw4LQtiq")
//上面的配置是规定了用户的用户名和密码,可以使用他们登录
//下面的配置是规定了这个用户的特定权限
//有了这个特定权限才可以访问特定的方法
.authorities("/user/get");
}
}
上面的代码中,我们使用tom和正确的密码是可以访问一般资源的,但是如果有特殊权限要求的资源是无法访问的,下面我们就将之前编写的UserController类中的方法改写一下
在设计请求路径时,可以在请求路径中使用{}框住某个名称,用于表示某个变量,后续,当客户端提交请求时,{}占位符对应的位置可以是任何数据,都会被匹配到!当请求路径中使用了{ }占位符,在处理请求的方法的参数列表中,在参数的声明之前添加@PathVariable注解即可获取到占位符的值!将核心参数放在URL中,这是一种RESTFUL风格的API。
@RestController
@RequestMapping("/test")
public class TestController {
@Autowired
private IUserService userService;
// http://localhost:8080/test/user/1
@GetMapping("/user/{id}")
public User getUserById(@PathVariable("id") Integer id) {
return userService.getById(id);
}
}
如果需要限制以上URL的访问,例如某些用户可以访问,但其他某些用户不可以访问,可以自行设计一个“权限字符串”,例如"a"
或"hello"
等均可!一般推荐使用URL的风格来定义访问权限,例如使用"test:user:info"
或"/user/user/info"
。
@GetMapping("/user/{id}")
@PreAuthorize("hasAuthority('test:user:info')")
public User getUserById(@PathVariable("id") Integer id) {
return userService.getById(id);
}
注意:权限字符串的设计与URL的设计没有任何关联!
可以在处理请求的方法之前配置@PreAuthorize
注解,用于声明“访问该请求路径时必须具备某种权限”,例如:
代码示例:
//实际访问路径为:localhost:8080/v1/users/get?id=1
@GetMapping("/get")
@PreAuthorize("hasAuthority('/user/get')")//设置访问这个方法的特殊权限
public User get(Integer id){
User u=userService.getById(id);
return u;
}
@GetMapping("/list")
@PreAuthorize("hasAuthority('/user/list')")
public List list(){
List list=userService.list();
return list;
}
关于以上注解配置:
注解名称@PreAuthorize表示“在处理请求之前验证权限”;
注解属性中的hasAuthority表示“需要具备某种权限”;
注解属性中的/user/list是自定义的权限字符串,只是一种标识。
Spring Security定义了UserDetailsService
接口,在接口存在抽象方法
UserDetailsService是SpringSecurity提供的一个接口,这个接口定义的方法返回一个UserDetails类型的对象,这个对象中包含用户的各种信息,用户名\密码\权限等
编写一个类来实现这个接口UserDetailsServiceImpl代码如下:
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
该方法的作用是:给定用户名,需要返回用户详情(UserDetails
类型的对象),Spring Security获取到该用户详情后,会自动完成用户身份的验证,包括验证成功之后的用户权限信息,都是由框架处理的,作为开发人员,只需要解决“根据用户名获取用户详情”的问题即可!
@Component
public class UserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String name)
throws UsernameNotFoundException {
UserDetails user=null;
//判断用户是不是jerry
if("jerry".equals(name)){
//设置jerry用户的详情
user= User.builder()
.username("jerry")
.password("{bcrypt}$2a$10$ytxmGeWcRZObqoDmlhnWxe6KqUjb9DTONmQVKkmwneHQtZw4LQtiq")
.authorities("/user/get")
.build();
}
return user;
}
}
注意:以上类必须在组件扫描的包中,并添加@Component
注解,则Spring框架会自动创建以上类的对象并管理,后续就可以直接装配这个类的对象了!
然后,回到SecurityConfig
类,应用以上类的对象:
然后,还需要在SecurityConfig类的声明之前添加@EnableGlobalMethodSecurity(prePostEnabled = true)注解,以允许执行访问权限的检查!例如:
@Configuration
//下面的注解表示开启Spring-Security的权限管理功能
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired(required = false)
UserDetailsServiceImpl userDetailsService;
//配置登录验证(即用户名和密码的验证)
@Override
protected void configure(AuthenticationManagerBuilder auth)
throws Exception {
auth.userDetailsService(userDetailsService);
}
}
严格意义上来说,以上方法并不是“登录”方法,只是一个“获取用户详情”的方法,甚至都不知道登录成功与否,所以,在参数列表中也没有密码,后续,将由Spring Security获取以上方法返回的对象,并验证密码是否正确等。
查询用户详细信息需要两个方法,一个是根据用户名查询用户信息,还要上面根据用户id查询权限,这样才能获取所有登录时需要的资料
在UserMapper接口 示例:
@Repository
public interface UserMapper extends BaseMapper {
// 根据用户的id查询用户的所有权限
@Select("SELECT p.id,p.name" +
"FROM user u" +
"LEFT JOIN user_role ur ON u.id=ur.user_id" +
"LEFT JOIN role r ON r.id=ur.role_id" +
"LEFT JOIN role_permission rp ON r.id=rp.role_id" +
"LEFT JOIN permission p ON p.id=rp.permission_id" +
"WHERE u.id=#{id}")
List findUserPermissionById(Integer id);
//按用户名查询用户
@Select("select * from user where username=#{username}")
User findUserByUsername(String username);
}
编写的程序的基本准则是由我们自己的Service层调用我们自己写的Mapper,这次调用比较特殊,不是控制器调用Service而是SpringSecurity来调用,我们首先在IUserService接口中声明一个方法
public interface IUserService extends IService {
//根据用户名获得用户认证信息的业务逻辑层方法
UserDetails getUserDetails(String username);
}
然后在实现类UserServiceImpl 中实现:
@Service
public class UserServiceImpl extends ServiceImpl implements IUserService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails getUserDetails(String username) {
//根据用户名查询出用户对象
User user=userMapper.findUserByUsername(username);
// 如果用户为空,表示用户名不存在,返回登录失败
if(user==null){
return null;
}
//根据用户id查询用户权限
List ps=userMapper
.findUserPermissionById(user.getId());
//将查询到的所有权限名称填装到一个数组中
String[] auths=new String[ps.size()];
int i=0;
for(Permission p: ps){
auths[i++]=p.getName();
}
//构建UserDetails
UserDetails u= org.springframework.security.core
.userdetails.User.builder()
.username(user.getUsername())
.password(user.getPassword())
.authorities(auths)
//getLocked默认都是0,所以要判==1得到false表示不锁定
.accountLocked(user.getLocked()==1)
//getEnabled默认都是1,所以要判==0得到false表示可用
.disabled(user.getEnabled()==0)
.build();
return u;//一定不要返回null!!!!
}
}
最后重构UserDetailsServiceImpl类中的代码 示例:
@Component
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private IUserService userService;
@Override
public UserDetails loadUserByUsername(String name)
throws UsernameNotFoundException {
UserDetails user=userService.getUserDetails(name);
return user;
}
}
控制授权范围和自定义登录页面授权范围控制
一个网站实际上并不是所有页面资源都需要登录才能访问,所以现在SpringSecurity的限制策略是比较严格的,如果我们想自己来设置这些授权范围就需要下面的代码,SecurityConfig类中添加一个方法代码如下
在SecurityConfig
中重写protected void configure(HttpSecurity http)
方法:
/设置SpringSecurity的授权范围和登录页面
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable() //关闭防跨域攻击
.authorizeRequests()//开始设置授权范围
.antMatchers(
"/index.html",
"/img/*",
"/js/*",
"/css/*",
"/bower_components/**",
"/login.html"
).permitAll() //上面路径允许直接访问
.anyRequest().authenticated()//其他的资源需要登录
.and().formLogin()//登录方式是表单
.loginPage("/login.html")//指定登录的页面
.loginProcessingUrl("/login")//当登录页面提交时,会提交给哪个路径
.failureUrl("/login.html?error")//登录失败从新登录,显示错误提示
.defaultSuccessUrl("/index.html")//登录成功跳转到index.html
.and().logout() //配置登出
.logoutUrl("/logout")//设置登出路径
//登出成功跳转登录页面,并提示已登出
.logoutSuccessUrl("/login.html?logout");
}
// 准备白名单,是不需要登录就可以访问的路径
// 授权设置,是相对固定的配置
// csrf().disable() > 关闭跨域攻击
// authorizeRequests() > 对请求进行授权
// antMatchers() > 配置访问白名单
// permitAll() > 对白名单中的路径进行授权
// anyRequest() > 其它的请求
// authenticated() > 仅经过授权的允许访问,也可以理解为“未被授权将不允许访问”
// and.formLogin() > 未被授权的将通过登录表单进行验证登录并授权
http
的链式方法的配置是相对固定的,可以尝试理解,也可以直接套用;以上调用的antMatchers( )
相当于使用SpringMVC拦截器时配置白名单,方法的参数中,应该将所有需要被直接放行(不登录即可访问的位置)的路径都添加进来,例如:
@Override
protected void configure(HttpSecurity http) throws Exception {
// 准备白名单,是不需要登录就可以访问的路径
// String[] antMatchers = {
// "/index.html"
//};
String[] antMatchers = {
"/index.html",
"/bower_components/**",
"/css/**",
"/img/**",
"/js/**"
};
http.csrf().disable()
.authorizeRequests()
.antMatchers(antMatchers).permitAll()
.anyRequest().authenticated()
.and().formLogin();
}
首先,在项目中添加Thymeleaf的依赖:
org.thymeleaf.extras
thymeleaf-extras-springsecurity5
org.springframework.boot
spring-boot-starter-thymeleaf
自定义的登录页面,将是被设计为HTML模版页,当请求登录的网址时,转发到该HTML模版页,则在项目的src/main/resoueces下创建templates文件夹,这是SpringBoot项目默认使用的模版页面文件夹,不需要配置,在转发时默认就会在这个文件夹中查询HTML模版文件,当文件夹创建完成后,将static文件夹下的login.html文件拖拽到templates文件夹下。
接下来,自定义控制器,设计登录页面的请求路径,在处理该路径的请求时,直接转发到/templates/login.html文件,由于Thymeleaf在整合时已经将前缀配置为了/templates/,把后缀配置为了.html,所以在控制器返回的视图名就是login:
在控制层新建一个控制器类用于显示login.html页面,名为SystemController代码如下
@RestController
public class SystemController {
@GetMapping("/login.html")
public ModelAndView loginForm(){
return new ModelAndView("login");
}
}
@Controller
public class SystemController {
@GetMapping("/login.html")
public String login() {
return "login";
}
// 适用于使用@RestController时
// public ModelAndView login() {
// return new ModelAndView("login");
// }
}