SpringBoot集成Spring Security安全框架
2018-03-30 02:33
696 查看
因为项目需要,第一次接触Spring Security,早就听闻Spring Security强大但上手困难,今天学习了一天,翻遍了全网资料,才仅仅出入门道,特整理这篇文章来让后来者少踩一点坑(本文附带实例程序,请放心食用)
预警:
如果你仅仅是学习一个安全框架,不推荐使用Spring Security!!!!推荐学习Apache Shiro,配置简单易上手,该有功能它都有,可以参考我的这篇文章:SpringBoot集成Shiro安全框架
重要:
写文章不易,转载请保留出处:https://blog.csdn.net/yuanlaijike/article/details/79751583
本篇文章环境:
本篇文章重点:
1.自动登陆(Remember me)
2.自定义表单登陆(图形验证码)
3.异常处理
一、Spring Security入门程序
1.1 导入依赖
1.2 准备数据库
1.3 准备页面
1.4 配置application.properties
1.5 创建实体、DAO、Service和Controller
1.5.1 实体
1.5.2 DAO
1.5.3 Service
1.5.4 Controller
1.6 配置Spring Security
1.6.1 AuthenticationProvider
1.6.2 WebSecurityConfig
1.7 运行程序
二、添加自动登录(Remember me)功能
2.1 修改login.html
2.2 修改WebSecurityConfig
2.3 配置UserDetailsService
2.3 修改CustomAuthenticationProvider
2.4 再次修改WebSecurityConfig
2.5 运行程序
2.6 解决no PasswordEncoder错误
三、自定义表单登陆认证(增加验证码功能)
3.1 注入Servlet
3.1.1 编写验证码Servlet
3.1.2 SpringBoot注入Servlet
3.2 修改login.html
3.3 允许匿名访问
3.4 WebAuthenticationDetails
3.4.1 自定义WebAuthenticationDetails
3.4.2 自定义AuthenticationDetailsSource
3.4.3 修改WebSecurityConfig
3.5 校验验证码
3.6 运行程序
四、异常处理
4.1 异常分析
4.2 源码分析
4.3 处理异常
五、权限(Permission)控制【Loading…】
六、源码与参考资料
这里我们先暂时不考虑权限,只考虑
创建权限表
创建用户-角色表
初始化一下数据:
博主有话说:
这里的权限格式为
login.html:
博主有话说:
在form表单中,用户名的name必须是
登陆请求可以自定义,后面会说
home.html:
博主有话说:
最后一行是开启数据库的下划线命名和实体类的驼峰命名的转换
(2)SysRole
(3)SysUserRole
(2)SysRoleMapper
(3)SysUserRoleMapper
(2)SysRoleService
(3)SysUserRoleService
博主有话说:
如代码所示,获取当前登录用户:
博主有话说:
如果小伙伴们看过别人的文章就会奇怪我怎么没有使用
小伙伴们不用担心,后面我将使用
然后将我们自定义的
第二个configure方法中对路径进行了设置,可配置项很多,这里只配置我用到的。
第三个个configure方法中可以配置对静态资源的放行。
要想实现自动登录功能十分简单,spring security会帮我们自动配置,我们仅需要做一些配置。
博主有话说:
这里自动登陆的name属性必须是
但是因为我们使用了
这是因为Spring Security想要开启自动登陆,必须得自定义实现
让我们自定义一个
我们需要重写
我们发现这个User的一个构造方法为:
前两个参数不用说,第三个参数是不是有点眼熟?这不就是我们在
我们来整理下思路:
(1)在
(2)在
这样我们就将获取用户数据库密码和权限的工作交给了
说了那么多,通过代码来体现吧:
我们首先将
完整的
这是因为Spring Security 5.0中强制必须指定PasswordEncoder,虽然我们完全不用它的加密方式,但是强制要求配,真的流氓!
修改
注:本例使用servlet后台生成验证码。
实现自定义表单登陆,除了使用我这种方法,还可以使用过滤器来实现。
我们设置了
我们在form表单中加入了验证码的img标签,其src为我们设置的验证码servlet,为其配置刷新方法和鼠标放上去的方法。
修改
现在尝试运行一下,已经能够显示验证码了,点击图片也可以刷新:
那么你自己加的验证码Spring Security怎么管理呢?它没有那么智能到能识别出这是你的验证码,这时候就要请出我们的主角——
那么我想要让spring security把我们输入的验证码也携带着,我们就需要自定义
在这个方法中,我们将前台form表单中的
首先
(1)当用户名不存在时手动抛出了
(2)验证码错误时手动抛出了
(3)密码输入错误时手动抛出了
除此之外还有:
(4)
(5)
(6)
以上列出的这些异常都是
(1)在
(2)在
(3)在
① 如果没有设置,直接返回401错误,即
② 如果设置了:首先执行
(4)在
至此spring security完成了异常处理,总结一下:
–> AbstractAuthenticationProcessingFilter
–> AbstractAuthenticationProcessingFilter.
–> SimpleUrlAuthenticationFailureHandler.
–> SimpleUrlAuthenticationFailureHandler.
(1)
(2)在controller中处理异常
我们首先获取了
运行程序,当我们输入错误验证码时:
错误密码时:
springboot_security 完整例子,是以下各个子例子的整合
springboot_security01 Spring Security入门程序
springboot_security02 添加自动登录(Remember me)功能
springboot_security03 自定义表单登陆认证(增加验证码功能)
springboot_security04 异常处理
参考资料:
Spring Boot基础教程
Spring Boot [集成-Spring Security]
在Spring Boot中整合Spring Security并自定义验证代码
Spring Security在登录验证中增加额外数据(如验证码)
在使用 Spring Security 的 Remember Me 记住密码功能时遇到的问题和解决方法
Spring Security(19)——对Acl的支持
Spring Boot Servlet
Spring Security inMemoryAuthentication 验证失败
Spring Security教程外篇(1)—- AuthenticationException异常详解
预警:
如果你仅仅是学习一个安全框架,不推荐使用Spring Security!!!!推荐学习Apache Shiro,配置简单易上手,该有功能它都有,可以参考我的这篇文章:SpringBoot集成Shiro安全框架
重要:
写文章不易,转载请保留出处:https://blog.csdn.net/yuanlaijike/article/details/79751583
本篇文章环境:
SpringBoot 2.0+
Mybatis+
Spring Security 5.0
本篇文章重点:
1.自动登陆(Remember me)
2.自定义表单登陆(图形验证码)
3.异常处理
一、Spring Security入门程序
1.1 导入依赖
1.2 准备数据库
1.3 准备页面
1.4 配置application.properties
1.5 创建实体、DAO、Service和Controller
1.5.1 实体
1.5.2 DAO
1.5.3 Service
1.5.4 Controller
1.6 配置Spring Security
1.6.1 AuthenticationProvider
1.6.2 WebSecurityConfig
1.7 运行程序
二、添加自动登录(Remember me)功能
2.1 修改login.html
2.2 修改WebSecurityConfig
2.3 配置UserDetailsService
2.3 修改CustomAuthenticationProvider
2.4 再次修改WebSecurityConfig
2.5 运行程序
2.6 解决no PasswordEncoder错误
三、自定义表单登陆认证(增加验证码功能)
3.1 注入Servlet
3.1.1 编写验证码Servlet
3.1.2 SpringBoot注入Servlet
3.2 修改login.html
3.3 允许匿名访问
3.4 WebAuthenticationDetails
3.4.1 自定义WebAuthenticationDetails
3.4.2 自定义AuthenticationDetailsSource
3.4.3 修改WebSecurityConfig
3.5 校验验证码
3.6 运行程序
四、异常处理
4.1 异常分析
4.2 源码分析
4.3 处理异常
五、权限(Permission)控制【Loading…】
六、源码与参考资料
一、Spring Security入门程序
1.1 导入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!-- Spring Security包 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- DAO包 --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency>
1.2 准备数据库
一般权限控制有三层,即:用户<-->角色<-->权限,用户与角色是多对多,角色和权限也是多对多。
这里我们先暂时不考虑权限,只考虑
用户<-->角色,创建用户表
sys_user:
CREATE TABLE `sys_user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL, `password` varchar(255) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
创建权限表
sys_role:
CREATE TABLE `sys_role` ( `id` int(11) NOT NULL, `name` varchar(255) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
创建用户-角色表
sys_user_role:
CREATE TABLE `sys_user_role` ( `user_id` int(11) NOT NULL, `role_id` int(11) NOT NULL, PRIMARY KEY (`user_id`,`role_id`), KEY `fk_role_id` (`role_id`), CONSTRAINT `fk_role_id` FOREIGN KEY (`role_id`) REFERENCES `sys_role` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT `fk_user_id` FOREIGN KEY (`user_id`) REFERENCES `sys_user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
初始化一下数据:
INSERT INTO `sys_role` VALUES ('1', 'ROLE_ADMIN'); INSERT INTO `sys_role` VALUES ('2', 'ROLE_USER'); INSERT INTO `sys_user` VALUES ('1', 'admin', '123'); INSERT INTO `sys_user` VALUES ('2', 'jitwxs', '123'); INSERT INTO `sys_user_role` VALUES ('1', '1'); INSERT INTO `sys_user_role` VALUES ('2', '2');
博主有话说:
这里的权限格式为
ROLE_XXX,是Spring Security规定的,不要乱起名字哦。
1.3 准备页面
因为是示例程序,页面越简单越好,只用于登陆的login.html以及用于登陆成功后的
home.html:
login.html:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>登陆</title> </head> <body> <h1>登陆</h1> <form method="post" action="/login"> <div> 用户名:<input type="text" name="username"> </div> <div> 密码:<input type="password" name="password"> </div> <div> <button type="submit">立即登陆</button> </div> </form> </body> </html>
博主有话说:
在form表单中,用户名的name必须是
username,密码的name必须是
password。
登陆请求可以自定义,后面会说
home.html:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>登陆成功</h1> <a href="/admin">检测ROLE_ADMIN角色</a> <a href="/user">检测ROLE_USER角色</a> <button onclick="window.location.href='/logout'">退出登录</button> </body> </html>
1.4 配置application.properties
spring.datasource.driver-class-name=com.mysql.jdbc.Driver spring.datasource.url=jdbc:mysql://localhost:3306/test3?useUnicode=true&characterEncoding=utf-8&useSSL=true spring.datasource.username=root spring.datasource.password=root #开启Mybatis下划线命名转驼峰命名 mybatis.configuration.map-underscore-to-camel-case=true
博主有话说:
最后一行是开启数据库的下划线命名和实体类的驼峰命名的转换
1.5 创建实体、DAO、Service和Controller
1.5.1 实体
(1)SysUserpackage jit.wxs.entity; /** * @author jitwxs * @date 2018/3/30 1:18 */ public class SysUser implements Serializable{ static final long serialVersionUID = 1L; private Integer id; private String name; private String password; // 省略getter/setter
(2)SysRole
package jit.wxs.entity; /** * @author jitwxs * @date 2018/3/30 1:20 */ public class SysRole implements Serializable { static final long serialVersionUID = 1L; private Integer id; private String name; // 省略getter/setter
(3)SysUserRole
package jit.wxs.entity; /** * @author jitwxs * @date 2018/3/30 1:20 */ public class SysUserRole implements Serializable { static final long serialVersionUID = 1L; private Integer userId; private Integer roleId; // 省略getter/setter
1.5.2 DAO
(1)SysUserMapperpackage jit.wxs.mapper; /** * @author jitwxs * @date 2018/3/30 1:21 */ @Mapper public interface SysUserMapper { @Select("SELECT * FROM sys_user WHERE id = #{id}") SysUser selectById(Integer id); @Select("SELECT * FROM sys_user WHERE name = #{name}") SysUser selectByName(String name); }
(2)SysRoleMapper
package jit.wxs.mapper; /** * @author jitwxs * @date 2018/3/30 1:23 */ @Mapper public interface SysRoleMapper { @Select("SELECT * FROM sys_role WHERE id = #{id}") SysRole selectById(Integer id); }
(3)SysUserRoleMapper
package jit.wxs.mapper; /** * @author jitwxs * @date 2018/3/30 1:24 */ @Mapper public interface SysUserRoleMapper { @Select("SELECT * FROM sys_user_role WHERE user_id = #{userId}") List<SysUserRole> listByUserId(Integer userId); }
1.5.3 Service
(1)SysUserServicepackage jit.wxs.service; /** * @author jitwxs * @date 2018/3/30 1:26 */ @Service public class SysUserService { @Autowired private SysUserMapper userMapper; public SysUser selectById(Integer id) { return userMapper.selectById(id); } public SysUser selectByName(String name) { return userMapper.selectByName(name); } }
(2)SysRoleService
package jit.wxs.service; /** * @author jitwxs * @date 2018/3/30 1:27 */ @Service public class SysRoleService { @Autowired private SysRoleMapper roleMapper; public SysRole selectById(Integer id){ return roleMapper.selectById(id); } }
(3)SysUserRoleService
package jit.wxs.service; /** * @author jitwxs * @date 2018/3/30 1:29 */ @Service public class SysUserRoleService { @Autowired private SysUserRoleMapper userRoleMapper; public List<SysUserRole> listByUserId(Integer userId) { return userRoleMapper.listByUserId(userId); } }
1.5.4 Controller
package jit.wxs.web; /** * @author jitwxs * @date 2018/3/30 1:30 */ @Controller public class LoginController { private Logger logger = LoggerFactory.getLogger(LoginController.class); @RequestMapping("/") public String showHome() { String name = SecurityContextHolder.getContext().getAuthentication().getName(); logger.info("当前登陆用户:" + name); return "home.html"; } @RequestMapping("/login") public String showLogin() { return "login.html"; } @RequestMapping("/admin") @ResponseBody @PreAuthorize("hasRole('ROLE_ADMIN')") public String printAdmin() { return "如果你看见这句话,说明你有ROLE_ADMIN角色"; } @RequestMapping("/user") @ResponseBody @PreAuthorize("hasRole('ROLE_USER')") public String printUser() { return "如果你看见这句话,说明你有ROLE_USER角色"; } }
博主有话说:
如代码所示,获取当前登录用户:
SecurityContextHolder.getContext().getAuthentication()
@PreAuthorize用于判断用户是否有指定权限,没有就不能访问,需要开启全局方法注解开关
1.6 配置Spring Security
1.6.1 AuthenticationProvider
我们使用自定义的AuthenticationProvider来为我们进行身份验证和授权:
package jit.wxs.security; /** * @author jitwxs * @date 2018/3/29 19:49 */ @Component public class CustomAuthenticationProvider implements AuthenticationProvider { @Autowired private SysUserService userService; @Autowired private SysRoleService roleService; @Autowired private SysUserRoleService userRoleService; @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { // 获取用户输入的用户名和密码 String inputName = authentication.getName(); String inputPassword = authentication.getCredentials().toString(); // 判断用户是否存在 SysUser user = userService.selectByName(inputName); if (user == null) { throw new UsernameNotFoundException("未找到指定用户"); } // 对密码进行验证,如有需要可以进行自定义加密解密的判断 if (!inputPassword.equals(user.getPassword())) { throw new BadCredentialsException("密码错误,登录失败!"); } // 添加权限 List<SysUserRole> userRoles = userRoleService.listByUserId(user.getId()); List<GrantedAuthority> grantedAuthorities = new ArrayList<>(); for (SysUserRole userRole : userRoles) { SysRole role = roleService.selectById(userRole.getRoleId()); grantedAuthorities.add(new SimpleGrantedAuthority(role.getName())); } return new UsernamePasswordAuthenticationToken(inputName, inputPassword, grantedAuthorities); } @Override public boolean supports(Class<?> authentication) { return authentication.equals(UsernamePasswordAuthenticationToken.class); } }
博主有话说:
如果小伙伴们看过别人的文章就会奇怪我怎么没有使用
UserDetailsService,是因为我的实际项目对密码的加密方式是自定义加密,使用
UserDetailsService我没有找到自定义密码加密方式的办法,而使用
AuthenticationProvider就能够和
shiro一样很方便的对密码进行验证。
小伙伴们不用担心,后面我将使用
UserDetailsService配合
AuthenticationProvider一起进行权限验证。
1.6.2 WebSecurityConfig
WebSecurityConfig是Spring Security的主要配置文件,首先一上来就是加上三个注解:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)开启全局方法权限,前面controller层的
@PreAuthorize要用到
然后将我们自定义的
CustomAuthenticationProvider注入进来,在第一个configure方法中作为参数。
第二个configure方法中对路径进行了设置,可配置项很多,这里只配置我用到的。
第三个个configure方法中可以配置对静态资源的放行。
package jit.wxs.security; /** * @author jitwxs * @date 2018/3/29 16:57 */ @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private CustomAuthenticationProvider customAuthenticationProvider; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.authenticationProvider(customAuthenticationProvider); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() // 如果有匿名的url,填在下面 // .antMatchers().permitAll() .anyRequest().authenticated() .and() // 设置登陆页 .formLogin().loginPage("/login") // 设置登陆成功页 .defaultSuccessUrl("/").permitAll() .and() .logout().permitAll(); http.csrf().disable(); } @Override public void configure(WebSecurity web) throws Exception { // 设置拦截忽略文件夹,可以对静态资源放行 web.ignoring().antMatchers("/css/**", "/js/**"); } }
1.7 运行程序
二、添加自动登录(Remember me)功能
让我们接着上面的例子继续前进!!要想实现自动登录功能十分简单,spring security会帮我们自动配置,我们仅需要做一些配置。
2.1 修改login.html
为登陆的form表单增加一条记住密码的checkbox:... <form method="post" action="/login"> <div> 用户名:<input type="text" name="username"> </div> <div> 密码:<input type="password" name="password"> </div> <div> <label><input type="checkbox" name="remember-me"/> Remember Me</label> <button type="submit">立即登陆</button> </div> </form> ...
博主有话说:
这里自动登陆的name属性必须是
remember-me,它和上面的
username和
password一样,都是spring security进行管理的,改了名字spring security就找不到了!!!
2.2 修改WebSecurityConfig
这里的修改很简单,只要在configure()方法中加一个
rememberMe()即可:
... @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() // 如果有允许匿名的url,填在下面 // .antMatchers().permitAll() .anyRequest().authenticated() .and() // 设置登陆页 .formLogin().loginPage("/login") // 设置登陆成功页 .defaultSuccessUrl("/").permitAll() .and() .logout().permitAll() .and() .rememberMe(); http.csrf().disable(); } ...
2.3 配置UserDetailsService
其实如果你一开始使用UserDetailsService来进行身份验证和授权,上面两步已经能够实现了自动登陆了。
但是因为我们使用了
AuthenticationProvider来做的,所以这个时候如果你运行程序并尝试自动登陆,会出现这个错误:
这是因为Spring Security想要开启自动登陆,必须得自定义实现
UserDetailsService类。(这里先别急着心中骂博主,为啥不直接用UserDetailsService来做,用了AuthenticationProvider还得用UserDetailsService)
让我们自定义一个
CustomUserDetailsService来实现
UserDetailsService:
/** * @author jitwxs * @date 2018/3/30 9:17 */ public class CustomUserDetailsService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { return null; } }
我们需要重写
loadUserByUsername方法,参数
s是登陆界面中的
username传过来的,即用户输入的用户名。返回值是
UserDetails,这是一个接口,一般使用它的子类
org.springframework.security.core.userdetails.User(当前你也可以自定义个UserDetails)。
我们发现这个User的一个构造方法为:
... public User(String username, String password, Collection<? extends GrantedAuthority> authorities) { this(username, password, true, true, true, true, authorities); } ...
前两个参数不用说,第三个参数是不是有点眼熟?这不就是我们在
CustomAuthenticationProvider中用来放权限的那个集合,只是把List改成了Collection而已:
... @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { .... // 添加权限 List<SysUserRole> userRoles = userRoleService.listByUserId(user.getId()); List<GrantedAuthority> grantedAuthorities = new ArrayList<>(); for (SysUserRole userRole : userRoles) { SysRole role = roleService.selectById(userRole.getRoleId()); grantedAuthorities.add(new SimpleGrantedAuthority(role.getName())); } ... } ...
我们来整理下思路:
(1)在
CustomUserDetailsService类,通过
loadUserByUsername()加载用户输入那个用户名对应在数据库中的密码、权限,并返回一个
UserDetails对象。
(2)在
CustomAuthenticationProvider类,在
authenticate()方法获取到用户输入的用户名和密码,然后调用
CustomUserDetailsService的
loadUserByUsername()方法,对
UserDetails中的密码和用户输入的密码进行比对。
这样我们就将获取用户数据库密码和权限的工作交给了
CustomUserDetailsService来做,而
CustomAuthenticationProvider只专心于密码的校验工作。
说了那么多,通过代码来体现吧:
package jit.wxs.security; /** * @author jitwxs * @date 2018/3/30 9:17 */ @Service("userDetailsService") public class CustomUserDetailsService implements UserDetailsService { @Autowired private SysUserService userService; @Autowired private SysRoleService roleService; @Autowired private SysUserRoleService userRoleService; @Override public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { Collection<GrantedAuthority> authorities = new ArrayList<>(); SysUser user = userService.selectByName(s); // 判断用户是否存在 if(user == null) { throw new UsernameNotFoundException("用户名不存在"); } // 添加权限 List<SysUserRole> userRoles = userRoleService.listByUserId(user.getId()); for (SysUserRole userRole : userRoles) { SysRole role = roleService.selectById(userRole.getRoleId()); authorities.add(new SimpleGrantedAuthority(role.getName())); } // 返回UserDetails实现类 return new User(user.getName(), user.getPassword(), authorities); } }
2.3 修改CustomAuthenticationProvider
上面说过我们将用户是否存在、获取数据库信息、加载权限放在了UserDetailsService中来做,那么我们的
AuthenticationProvider只专心于密码的校验了,修改
CustomAuthenticationProvider:
package jit.wxs.security; /** * @author jitwxs * @date 2018/3/29 19:49 */ @Component public class CustomAuthenticationProvider implements AuthenticationProvider { @Autowired private CustomUserDetailsService customUserDetailsService; @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { // 获取用户输入的用户名和密码 String inputName = authentication.getName(); String inputPassword = authentication.getCredentials().toString(); // userDetails一定非空,否则会抛异常 UserDetails userDetails = customUserDetailsService.loadUserByUsername(inputName); // 对密码进行验证,如有需要可以进行自定义加密解密的判断 if (!inputPassword.equals(userDetails.getPassword())) { throw new BadCredentialsException("密码错误,登录失败!"); } return new UsernamePasswordAuthenticationToken(inputName, inputPassword, userDetails.getAuthorities()); } @Override public boolean supports(Class<?> authentication) { return authentication.equals(UsernamePasswordAuthenticationToken.class); } }
我们首先将
UserDetailsService注入进来,调用
loadUserByUsername()就能获取到UserDetails 对象,然后只需验证密码即可。
2.4 再次修改WebSecurityConfig
在2.1节中我们已经修改过了一次WebSecurityConfig,加入了rememberMe(),这里我们还需要将上面自定义的UserDetailsService注入进来,并交给
config去配置:
package jit.wxs.security; /** * @author jitwxs * @date 2018/3/29 16:57 */ ... public class WebSecurityConfig extends WebSecurityConfigurerAdapter { ... // 注入userDetailsService @Autowired private CustomUserDetailsService userDetailsService; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { ... // 将我们自定义的userDetailsService设置进去 auth.userDetailsService(userDetailsService); } ... }
完整的
WebSecurityConfig如下:
package jit.wxs.security; /** * @author jitwxs * @date 2018/3/29 16:57 */ @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private CustomAuthenticationProvider customAuthenticationProvider; @Autowired private CustomUserDetailsService userDetailsService; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.authenticationProvider(customAuthenticationProvider); auth.userDetailsService(userDetailsService); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() // 如果有允许匿名的url,填在下面 // .antMatchers().permitAll() .anyRequest().authenticated() .and() // 设置登陆页 .formLogin().loginPage("/login") // 设置登陆成功页 .defaultSuccessUrl("/").permitAll() .and() .logout().permitAll() .and() .rememberMe(); http.csrf().disable(); } @Override public void configure(WebSecurity web) throws Exception { // 设置拦截忽略文件夹,可以对静态资源放行 web.ignoring().antMatchers("/css/**", "/js/**"); } }
2.5 运行程序
配置了一圈,我们终于可以运行我们的程序了,当我们勾选自动登陆后,会在cookie中保存一个remember-me:
2.6 解决no PasswordEncoder错误
当我们的用户名输入正确时没有问题,但是一但输错了,就会出现java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"的错误:
这是因为Spring Security 5.0中强制必须指定PasswordEncoder,虽然我们完全不用它的加密方式,但是强制要求配,真的流氓!
修改
WebSecurityConfig,指定
PasswordEncoder,因为我们不用它的加密方式,使用自己的加密方式,所以直接弄一个不加密的
PasswordEncoder即可:
... @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.authenticationProvider(customAuthenticationProvider); auth.userDetailsService(userDetailsService) .passwordEncoder(new PasswordEncoder() { @Override public String encode(CharSequence charSequence) { return charSequence.toString(); } @Override public boolean matches(CharSequence charSequence, String s) { return s.equals(charSequence.toString()); } }); } ...
三、自定义表单登陆认证(增加验证码功能)
博主有话说:注:本例使用servlet后台生成验证码。
实现自定义表单登陆,除了使用我这种方法,还可以使用过滤器来实现。
3.1 注入Servlet
3.1.1 编写验证码Servlet
我们创建一个VerifyServlet,继承与
HttpServlet,这是用来生成验证码的,因为代码太长,请前往源码查看。
3.1.2 SpringBoot注入Servlet
SpringBoot配置自定义Servlet有两种方式,我们使用最简单的注解注入即可。修改Application启动类:
package jit.wxs; @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } /** * 注入验证码servlet */ @Bean public ServletRegistrationBean indexServletRegistration() { ServletRegistrationBean registration = new ServletRegistrationBean(new VerifyServlet()); registration.addUrlMappings("/getVerifyCode"); return registration; } }
我们设置了
VerifyServlet这个类的拦截url为
/getVerifyCode,这样关于Servlet就配置好了。
3.2 修改login.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>登陆</title> </head> <body> <h1>登陆</h1> <form method="post" action="/login"> <div> 用户名:<input type="text" name="username"> </div> <div> 密码:<input type="password" name="password"> </div> <div> <input type="text" class="form-control" name="verifyCode" required="required" placeholder="验证码"> <img src="getVerifyCode" title="看不清,请点我" onclick="refresh(this)" onmouseover="mouseover(this)" /> </div> <div> <label><input type="checkbox" name="remember-me"/> Remember Me</label> <button type="submit">立即登陆</button> </div> </form> <script> function refresh(obj) { obj.src = "getVerifyCode?" + Math.random(); } function mouseover(obj) { obj.style.cursor = "pointer"; } </script> </body> </html>
我们在form表单中加入了验证码的img标签,其src为我们设置的验证码servlet,为其配置刷新方法和鼠标放上去的方法。
3.3 允许匿名访问
不要忘记我们的Spring Security会拦截未登录情况下所有未配置匿名访问的url,所以我们要将验证码的url加入匿名url中,这样在未登录状态下才能访问该url。修改
WebSecurityConfig,允许
/getVerifyCode的匿名访问:
@Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() // 如果有允许匿名的url,填在下面 .antMatchers("/getVerifyCode").permitAll() .anyRequest().authenticated() .and() // 设置登陆页 .formLogin().loginPage("/login") // 设置登陆成功页 .defaultSuccessUrl("/").permitAll() .and() .logout().permitAll() .and() .rememberMe(); http.csrf().disable(); }
现在尝试运行一下,已经能够显示验证码了,点击图片也可以刷新:
3.4 WebAuthenticationDetails
我们知道form表单中的每一项都是交给Spring Security进行管理的,它根据username获取到了用户名,根据
password获取到了密码,根据
remember-be判断是否自动登陆。。。
那么你自己加的验证码Spring Security怎么管理呢?它没有那么智能到能识别出这是你的验证码,这时候就要请出我们的主角——
WebAuthenticationDetails。
WebAuthenticationDetails: 该类提供了获取用户登录时携带的额外信息的功能,默认提供了
remoteAddress与
sessionId信息。
那么我想要让spring security把我们输入的验证码也携带着,我们就需要自定义
WebAuthenticationDetails了。
3.4.1 自定义WebAuthenticationDetails
package jit.wxs.security; import org.springframework.security.web.authentication.WebAuthenticationDetails; import javax.servlet.http.HttpServletRequest; /** * 获取用户登录时携带的额外信息 * * @author jitwxs * @date 2018/3/30 11:02 */ public class CustomWebAuthenticationDetails extends WebAuthenticationDetails { private static final long serialVersionUID = 6975601077710753878L; private final String verifyCode; public CustomWebAuthenticationDetails(HttpServletRequest request) { super(request); verifyCode = request.getParameter("verifyCode"); } public String getVerifyCode() { return this.verifyCode; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append(super.toString()).append("; VerifyCode: ").append(this.getVerifyCode()); return sb.toString(); } }
在这个方法中,我们将前台form表单中的
verifyCode获取到,并通过get方法方便被调用。
3.4.2 自定义AuthenticationDetailsSource
自定义了WebAuthenticationDetails,我i们还需要将其放入
AuthenticationDetailsSource中,因此还得自定义实现
AuthenticationDetailsSource:
package jit.wxs.security; import org.springframework.security.authentication.AuthenticationDetailsSource; import org.springframework.security.web.authentication.WebAuthenticationDetails; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; /** * 该接口用于在Spring Security登录过程中对用户的登录信息的详细信息进行填充 * * @author jitwxs * @date 2018/3/30 11:08 */ @Component public class CustomAuthenticationDetailsSource implements AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> { @Override public WebAuthenticationDetails buildDetails(HttpServletRequest request) { return new CustomWebAuthenticationDetails(request); } }
3.4.3 修改WebSecurityConfig
首先将AuthenticationDetailsSource注入进来,然后在configure()方法中添加设置:
package jit.wxs.security; /** * @author jitwxs * @date 2018/3/29 16:57 */ ... public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> authenticationDetailsSource; ... @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() // 如果有允许匿名的url,填在下面 .antMatchers("/getVerifyCode").permitAll() .anyRequest().authenticated() .and() // 设置登陆页 .formLogin().loginPage("/login") // 设置登陆成功页 .defaultSuccessUrl("/").permitAll() // 指定authenticationDetailsSource .authenticationDetailsSource(authenticationDetailsSource) .and() .logout().permitAll() .and() .rememberMe(); http.csrf().disable(); } ...
3.5 校验验证码
我们在AuthenticationProvider中首先得到
CustomWebAuthenticationDetails对象,然后调用get方法就能够取到前台传过来的验证码了:
package jit.wxs.security; /** * @author jitwxs * @date 2018/3/29 19:49 */ @Component public class CustomAuthenticationProvider implements AuthenticationProvider { ... @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { ... CustomWebAuthenticationDetails details = (CustomWebAuthenticationDetails) authentication.getDetails(); String verifyCode = details.getVerifyCode(); if(!validateVerify(verifyCode)) { throw new DisabledException("验证码输入错误"); } ... } private boolean validateVerify(String inputVerify) { //获取当前线程绑定的request对象 HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); // 不分区大小写 // 这个validateCode是在servlet中存入session的名字 String validateCode = ((String) request.getSession().getAttribute("validateCode")).toLowerCase(); inputVerify = inputVerify.toLowerCase(); System.out.println("验证码:" + validateCode + "用户输入:" + inputVerify); return validateCode.equals(inputVerify); } ... }
3.6 运行程序
四、异常处理
不知道你有没有注意到,当我们登陆失败时候,spring security帮我们跳转到了
/login?error,奇怪额是不管是控制台还是网页上都没有打印错误信息。回答一下:
首先
/login?error是spring security默认的失败url,其次如果你不手动处理这个异常,这个异常是不会被处理的。
4.1 异常分析
我们先来列举下我们使用到了哪些异常:(1)当用户名不存在时手动抛出了
UsernameNotFoundException(用户找不到)
(2)验证码错误时手动抛出了
DisabledException(账户不可用)
(3)密码输入错误时手动抛出了
BadCredentialsException(坏的凭据)
除此之外还有:
(4)
LockedException(账户锁定)
(5)
AccountExpiredException(账户过期)
(6)
CredentialsExpiredException(证书过期)
以上列出的这些异常都是
AuthenticationException的子类,我们来看看
spring security如何处理
AuthenticationException的。
4.2 源码分析
异常处理一般在过滤器中处理,我们在AbstractAuthenticationProcessingFilter中找到了对
AuthenticationException的处理:
(1)在
doFilter()中,捕捉了
AuthenticationException异常,并交给了
unsuccessfulAuthentication()处理。
(2)在
unsuccessfulAuthentication()中,转交给了
SimpleUrlAuthenticationFailureHandler类的
onAuthenticationFailure()处理。
(3)在
onAuthenticationFailure()中,首先判断有没有设置
defaultFailureUrl。
① 如果没有设置,直接返回401错误,即
HttpStatus.UNAUTHORIZED的值。
② 如果设置了:首先执行
saveException()方法。然后判断
forwardToDestination,即是否是服务器跳转,默认使用重定向即客户端跳转。
(4)在
saveException()方法中,首先判断
forwardToDestination,如果使用服务器跳转则写入Request,客户端跳转则写入Session。写入名为
SPRING_SECURITY_LAST_EXCEPTION,值为
AuthenticationException。
至此spring security完成了异常处理,总结一下:
–> AbstractAuthenticationProcessingFilter
.doFilter()
–> AbstractAuthenticationProcessingFilter.
unsuccessfulAuthentication()
–> SimpleUrlAuthenticationFailureHandler.
onAuthenticationFailure()
–> SimpleUrlAuthenticationFailureHandler.
saveException()
4.3 处理异常
上面源码说了那么多,真正处理起来很简单,我们只需要指定错误的url,然后再该方法中对异常进行处理即可。(1)
WebSecurityConfig中添加
.failureUrl("/login/error")
... @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() // 如果有允许匿名的url,填在下面 .antMatchers("/getVerifyCode").permitAll() .anyRequest().authenticated() .and() // 设置登陆页 .formLogin().loginPage("/login") // 设置登陆成功页 .defaultSuccessUrl("/") // 设置登陆失败页 .failureUrl("/login/error") .permitAll() // 指定authenticationDetailsSource .authenticationDetailsSource(authenticationDetailsSource) .and() .logout().permitAll() .and() .rememberMe(); http.csrf().disable(); } ...
(2)在controller中处理异常
@RequestMapping("/login/error") public void loginError(HttpServletRequest request, HttpServletResponse response) { response.setContentType("text/html;charset=utf-8"); AuthenticationException exception = (AuthenticationException)request.getSession().getAttribute("SPRING_SECURITY_LAST_EXCEPTION"); try { response.getWriter().write(exception.toString()); }catch (IOException e) { e.printStackTrace(); } }
我们首先获取了
saveException()中存入session的
SPRING_SECURITY_LAST_EXCEPTION。为了演示,我只是简单的将错误信息返回给了页面。
运行程序,当我们输入错误验证码时:
错误密码时:
五、权限(Permission)控制【Loading…】
六、源码与参考资料
项目地址:GitHub:springboot_samplespringboot_security 完整例子,是以下各个子例子的整合
springboot_security01 Spring Security入门程序
springboot_security02 添加自动登录(Remember me)功能
springboot_security03 自定义表单登陆认证(增加验证码功能)
springboot_security04 异常处理
参考资料:
Spring Boot基础教程
Spring Boot [集成-Spring Security]
在Spring Boot中整合Spring Security并自定义验证代码
Spring Security在登录验证中增加额外数据(如验证码)
在使用 Spring Security 的 Remember Me 记住密码功能时遇到的问题和解决方法
Spring Security(19)——对Acl的支持
Spring Boot Servlet
Spring Security inMemoryAuthentication 验证失败
Spring Security教程外篇(1)—- AuthenticationException异常详解
相关文章推荐
- SpringSide 3 中的安全框架(spring security)-这是我看过对springsecurity分析的最好的 最清晰的文章
- AdminEAP框架-集成Shiro安全认证
- Spring Security安全框架入门篇
- Spring Security 2.0 安全框架 使用及文件配置说明
- Spring Security安全框架入门篇
- Spring Security安全框架入门篇
- SSM集成安全框架shiro
- Spring Security3安全框架的学习
- spring security 一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架
- springboot集成spring security初探2--从数据库读取用户权限
- Spring安全框架 Spring Security
- Spring Security 安全框架
- SpringBoot28-springboot企业级开发-安全控制Spring Security
- 安全框架Shiro和Spring Security比较
- 安全框架Shiro和Spring Security比较
- Spring Security --- 权限控制安全框架入门简介
- 安全框架Shiro和Spring Security比较
- Spring Security安全框架——session管理(并发单点登陆等)
- spring源码剖析之Spring Security安全框架
- Spring Boot集成Jasypt安全框架