您的位置:首页 > 编程语言 > Java开发

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

本篇文章环境:
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)SysUser

package 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)SysUserMapper

package 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)SysUserService

package 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_sample

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异常详解
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息