Spring Security实现表单提交操作,连接数据库,实现注册和登录
Spring Security 是 Spring 家族中的一个安全管理框架,实际上,在 Spring Boot 出现之前,Spring Security 就已经发展了多年了,但是使用的并不多,安全管理这个领域,一直是 Shiro 的天下。
相对于 Shiro,在 SSM/SSH 中整合 Spring Security 都是比较麻烦的操作,所以,Spring Security 虽然功能比 Shiro 强大,但是使用反而没有 Shiro 多(Shiro 虽然功能没有 Spring Security 多,但是对于大部分项目而言,Shiro 也够用了)。
自从有了 Spring Boot 之后,Spring Boot 对于 Spring Security 提供了 自动化配置方案,可以零配置使用 Spring Security。
因此,一般来说,常见的安全管理技术栈的组合是这样的:
SSM + Shiro
Spring Boot/Spring Cloud + Spring Security
注意,这只是一个推荐的组合而已,如果单纯从技术上来说,无论怎么组合,都是可以运行的。
我们来看下具体使用。
一、创建一个springboot项目,并引入相关依赖
<!--springsecurity需要引入的依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--jpa依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
实现登录
一些基本的DAO层的类不写了。
继承WebSecurityConfigurerAdapter类
WebSecurityConfigurerAdapter 是SpringSecurity 提供的用于我们扩展自己的配置
实现WebSecurityConfigurerAdapter经常需要重写的:
1、configure(AuthenticationManagerBuilder auth);
2、configure(WebSecurity web);
3、configure(HttpSecurity http);
//WebSecurityConfigurerAdapter 是SpringSecurity 提供的用于我们扩展自己的配置
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
UserDetailsServiceImpl userDetailsService;
//该方法的主要用法
//1.通过Java的方式 配置用户名/密码
//2.在这里完成获得数据库中的用户信息
//3.密码一定要加密(加密的方式一定要和注册是加密的方式一致)
//4.登录认证
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//下面这两行配置表示在内存中配置了两个用户
// auth.inMemoryAuthentication()
// .withUser("admin").roles("admin").password("a")
// .and()
// .withUser("user").roles("user").password("a");
//Spring Security 提供了BCryptPasswordEncoder类,实现Spring的PasswordEncoder接口使用BCrypt强哈希方法来加密密码。
auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}
//可以自定义加密方式,实现PasswordEncoder接口,
// springsercurity后面的版本必须指定PasswordEncoder实现类,但如果不想加密的话,也可以通过空实现的方式
// @Bean
// PasswordEncoder passwordEncoder() {
// return new JWTPasswordEncoder();
// }
@Override
protected void configure(HttpSecurity http) throws Exception {
//http.addFilterBefore(verifyCodeFilter, UsernamePasswordAuthenticationFilter.class);
http
.authorizeRequests()//开启登录配置
// 如果有允许匿名的url,填在下面,这些url可以不经过过滤
.antMatchers("/login.html","/register.html","/register").permitAll()
//注意该处要与数据库的ROLE_后面部分保持一致,大小写也要一致
.antMatchers("/hello").hasRole("ADMIN")//表示访问 /hello 这个接口,需要具备 ADMIN 这个角色
.anyRequest().authenticated()//表示剩余的其他接口,任何用户登录之后就能访问
.and()
//开启表单登录,该方法会应用 FormLoginConfigurer 到HttpSecurity上,后续会被转换为对应的Filter
.formLogin()
//定义登录页面,未登录时,访问一个需要登录之后才能访问的接口,会自动跳转到该页面
.loginPage("/login.html")//默认跳转的是springsecurity自带的登录界面
//默认是 /login,但是当配置了.loginPage("/login.html"),默认值就变成了/login.html
.loginProcessingUrl("/doLogin")
// 设置登陆成功页
.defaultSuccessUrl("/index.html")
//定义登录时,用户名的 key,默认为 username
.usernameParameter("uname")
// 定义登录时,用户密码的 key,默认为 password
.passwordParameter("password")
// 登录成功的处理器
// .successHandler(new AuthenticationSuccessHandler() {
// @Override
// public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
// resp.setContentType("application/json;charset=utf-8");
// resp.sendRedirect("index.html");
// PrintWriter out = resp.getWriter();
// out.write("success");
// out.flush();
//
// }
// })
//登录失败的处理器
.failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException exception) throws IOException, ServletException, IOException {
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
out.write("fail");
out.flush();
}
})
//和表单登录相关的接口统统都直接通过
.permitAll()
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessHandler(new LogoutSuccessHandler() {
@Override
public void onLogoutSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
out.write("logout success");
out.flush();
}
})
.permitAll()
.and()
.httpBasic()
.and()
.csrf().disable();// 关闭CSRF跨域
}
//直接过滤掉该地址,即该地址不走 Spring Security 过滤器链
@Override
public void configure(WebSecurity web) throws Exception {
// 设置拦截忽略文件夹,可以对静态资源放行
web.ignoring().antMatchers("/css/**", "/js/**","/register.html");
}
}
需要注意的是loginPage和loginProcessingUrl,默认值都是 /login。如果只配置loginPage而不配置loginProcessingUrl的话。那么loginProcessingUrl默认就是loginPage的值
如果只配置loginProcessUrl,就会用不了自定义登陆页面,security会使用自带的默认登陆页面。换句话说,如果跳转页面是/login,提交表单/toLogin,那最好就是loginPage配/login,loginProcessingUrl配/toLogin
loginPage就是登录页面的url地址loginProcessingUrl是验证的登录url地址 如果不设则默认和loginPage一样。
实现UserDetailsService接口,这一步是非常重要的!!
整个Spring Security连接数据库的核心就是实现UserDetaisService接口
重写接口loadUserByUsername这个方法,在其中完成数据库的查询工作,并将得到的admin返回就可以了
实现UserDetailsService接口
整个Spring Security连接数据库的核心就是实现UserDetaisService接口
重写接口loadUserByUsername这个方法,在其中完成数据库的查询工作,并将得到的角色权限返回。
@Service
//重写UserDetailsService的loadUserByUsername方法
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
UserDao mapper;
PasswordEncoder passwordEncoder=new BCryptPasswordEncoder();
@Override
public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {
User user =mapper.findByusername(name);
if (user==null){
return null;
}else {
//创建一个权限的集合
Collection<GrantedAuthority> authorities = new ArrayList<>();
//添加获取权限
authorities.add(new SimpleGrantedAuthority(user.getRole()));
//把对象信息(用户名,密码,权限)存入对象,返回该对象,controller层直接调用
//如果数据库未加密需要添加以下注释的两行代码,但一般来说数据库是不能用明文来存密码的,太不安全了
// org.springframework.security.core.userdetails.User user2 =new org.springframework.security.core.userdetails.User(user.getUsername(), passwordEncoder.encode(user.getPwd()), authorities);
org.springframework.security.core.userdetails.User user2 =new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPwd(), authorities);
// System.out.println("管理员信息:"+user.getUsername()+" "+passwordEncoder.encode(user.getPwd())+" "+user2.getAuthorities());
return user2;
}
}
}
可以看到上面有两个User类,因为当前建立实体类的时候,没有考虑到springsecurity自带了一个类,也叫User,专门用来用户名,密码和权限的,后面也就不想改了。
<h1>登录界面</h1>
<form action="doLogin" method="post">
<!--要与配置类的属性名一致-->
用户名:<input type="text" name="uname">
密码:<input type="text" name="password">
<input type="submit" value="登录">
</form>
注册加密,存入数据库
UserController类
@GetMapping("/register")
public String register(String uname,String password){
User user=new User();
user.setUsername(uname);
user.setPwd(password);
user.setRole("ROLE_USER");
if(userBiz.insert(user)==true){
return "login.html";
}else{
return "register.html";
}
}
UserBiz类
public boolean insert(User user){
encryptPassword(user);
if(userDao.save(user)!=null)
return true;
else
return false;
};
private void encryptPassword(User user){
String password = user.getPwd();
password = new BCryptPasswordEncoder().encode(password);
System.out.println(password);
user.setPwd(password);
}
<h1>注册界面</h1>
<!-- 需要在springsecrurity处,排除掉springsecrurity对register的请求过滤-->
<form action="/register" method="post">
<!--要与配置类的属性名一致-->
用户名:<input type="text" name="uname">
密码:<input type="text" name="password">
<input type="submit" value="注册">
</form>
关于加密
任何应用考虑到安全,绝不能明文的方式保存密码到数据库。密码应该通过哈希算法进行加密。
有很多标准的算法比如SHA或者MD5,结合salt(盐)是一个不错的选择。
Spring Security提供BCryptPasswordEncoder类,实现Spring的PasswordEncoder接口使用BCrypt强哈希方法来加密密码。BCrypt强哈希方法 每次加密的结果都不一样
既然每次加密结果不一样,就不可以通过相同字符串加密后的结果来判定是不是同一个字符串了,这就更加增强了安全性。
如果需要判断是否是原来的密码,需要用它自带的方法。
加密:
BCryptPasswordEncoder encode = new BCryptPasswordEncoder();
encode.encode(password);
判断:
需要通过自带的方法 matches 将未经过加密的密码和已经过加密的密码传进去进行判断,返回布尔值。
encode.matches(oldpassword,user1.getPassword());
数据库端的前缀
在角色权限字段前必须加"ROLE_"否则识别不了,或者可以采取自定义等方式。
项目实例(结合Spring-Security图片验证码)
https://github.com/tph-lab/TPHDisk