接着上一章节,我们在这一章种讨论如何在现有的ssm框架中加入security机制,说白了,就是为我们项目提供身份验证的功能。现有的需求中大多项目都无法脱离登录注册功能。如果开发时每个模块提供一个登录注册功能,整个项目就会臃肿不堪,单点登录也就应用而生了。至于OAuth2与springBoot的结合我们在随后章节讨论,这一章节讨论security机制的简单应用。
In-Memory Authentication
基于内存的身份认证功能。也就是说身份信息是保存到内存中。这种方式了解为主,在实际开发中使用较少。
1 搭建ssm+springsecurity框架
需要的依赖有
- web(spring mvc),
- mybatis(mybatis数据库),
- mysql(mysql数据库驱动),
- security(安全校验机制)
> spring init -g=com.briup.apps -a=app04 -p=war -d=web,mybatis,mysql,security app04
> cd app04
> mvn install
构建项目过程中依旧会报没有指定驱动类的异常,解决方案还是按照上一章节的方式,在application.properties中进行配置,然后在pom.xml中配置热部署的依赖(方便开发)
配置就绪后启动项目
> mvn spring-boot:run
哈,是不是有些意外,我们就没做什么事情,竟然具有授权的功能了,这是security默认帮我们实现的功能,那么用户名密码是什么呢? 用户名默认为user,密码在启动项目的时候会打印到控制台。
如果我们直接点击取消,提示未授权异常。
刷新页面后进行登录。输入user/console中密码,出现如下错误,不过这个错误我们是能理解的,404找不到,说明没有配置服务。
2 自定义授权
在默认授权管理中如果我们想添加用户改怎么办?如果我们想自定义登录页面怎么办?如果我们想自定义拦截怎么办?
2.1 添加自定义用户
实际上我们项目之所有具有授权功能,是security框架帮我们实现的。也就是WebSecurityConfigurerAdapter这个适配器完成,如果想要改变其默认行为,那可以重写该适配器中的一些方法。
/**
* 自定义身份验证类(用于重写WebSecurityConfigurerAdapter默认配置)
* @Configuration 表示这是一个配置类
* @EnableWebSecurity 允许security
* configure() 该方法重写了父类的方法,用于添加用户与角色
* */
@Configuration
@EnableWebSecurity
public class AuthConfig extends WebSecurityConfigurerAdapter {
/**
* 重写该方法,添加自定义用户
* */
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("admin").password("admin").roles("ADMIN")
.and()
.withUser("terry").password("terry").roles("USER")
.and()
.withUser("larry").password("larry").roles("USER");
}
}
重启服务进行测试
当用户名密码输入错误的时候,出现以下界面
当用户名密码输入正确的时候,是可以继续访问服务,由于我们还么有提供任何服务,所有均会出现404异常。
2.2 提供服务
订单控制器 OrderController
@RestController
@RequestMapping("/orders")
public class OrderController {
@GetMapping("/findAll")
public String findAll() {
return "findAll";
}
}
用户管理控制器 UserController
@RestController
@RequestMapping("/users")
public class UserController {
@GetMapping("/findAll")
public String findAll() {
return "user list";
}
}
紧接着在AuthConfig 中配置权限。
@Configuration
@EnableWebSecurity
public class AuthConfig extends WebSecurityConfigurerAdapter {
/**
* 重写该方法,设定用户访问权限
* 用户身份可以访问 订单相关API
* */
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/orders/**").hasRole("USER") //用户权限
.antMatchers("/users/**").hasRole("ADMIN") //管理员权限
.antMatchers("/login").permitAll()
.and()
.formLogin();
//super.configure(http);
}
/**
* 重写该方法,添加自定义用户
* */
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("admin").password("admin").roles("ADMIN","USER")
.and()
.withUser("terry").password("terry").roles("USER")
.and()
.withUser("larry").password("larry").roles("USER");
}
}
重启服务进行登录
- 如果使用admin账号登录则users相关API和orders相关API都可以访问
- 如果使用terry账号登录则只能访问orders相关API
3 自定义登录页面
默认情况下,当用户没有登录就去访问受保护资源时,系统会默认请求/login(get方式),这时重定向到登录页(spring security自带)。当输入用户名密码点击登录按钮的时候,系统会请求/login(post方式)。现在我们希望自定义登录页面(默认的登录页面很丑),但是身份校验还是希望由security来进行。这时候我们只需要将登录页面重定向到我们自定义页面即可,这时候DIY表单,但是在这里切记一点。登录页面重定向的地址和表单提交的地址务必一致!
- 自定义配置 AuthConfig
在原来的基础上扩展了DIY登录页面的控制器的设置
/**
* 自定义身份验证类(用于重写WebSecurityConfigurerAdapter默认配置)
* @Configuration 表示这是一个配置类
* @EnableWebSecurity 允许security
* configure() 该方法重写了父类的方法,用于添加用户与角色
* */
@Configuration
@EnableWebSecurity
public class AuthConfig extends WebSecurityConfigurerAdapter {
/**
* 重写该方法,设定用户访问权限
* 用户身份可以访问 订单相关API
* */
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/orders/**").hasRole("USER") //用户权限
.antMatchers("/users/**").hasRole("ADMIN") //管理员权限
.and()
.formLogin()
.loginPage("/login") //跳转登录页面的控制器,该地址要保证和表单提交的地址一致!
.permitAll()
.and()
.logout()
.permitAll()
.and()
.csrf().disable(); //暂时禁用CSRF,否则无法提交表单
}
/**
* 重写该方法,添加自定义用户
* */
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("admin").password("admin").roles("ADMIN","USER")
.and()
.withUser("terry").password("terry").roles("USER")
.and()
.withUser("larry").password("larry").roles("USER");
}
}
- 添加 login(get方式)控制器
即如果用户没有登录就访问受保护的资源,系统将会进行拦截,拦截之后会请求/login(get方式),然后经过我们这个控制器跳转到DIY登录页面。
@Controller
@RequestMapping("/")
public class IndexController {
@GetMapping("/login")
public String login(Model model, @RequestParam(value = "error", required = false) String error) {
if (error != null) {
model.addAttribute("error", "用户名或密码错误");
}
return "forward:/login_page.html";
}
}
- 登录页面 (login_page.html)
注意:这里表单的action为 /login 提交方式为POST
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录页面</title>
</head>
<body>
<h2>自定义登录页面</h2>
<hr>
<form action="/login" method="POST" name="f">
用户名<input type="text" name="username"/> <br>
密码 <input type="password" name="password"> <br>
<input type="submit" value="登录">
</form>
</body>
</html>
当需要登录的时候,会跳转到login_page.html中,至此完成自定义登录页面设置
4 登录后续操作
这里我只是简单处理了一下,通过SecurityContextHolder获取目前登录的用户信息,然后将其放到session中(不建议如此处理)然后将页面重定向到首页中。
@Configuration
@EnableWebSecurity
public class AuthConfig extends WebSecurityConfigurerAdapter {
/**
* 重写该方法,设定用户访问权限
* 用户身份可以访问 订单相关API
* */
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/orders/**").hasRole("USER") //用户权限
.antMatchers("/users/**").hasRole("ADMIN") //管理员权限
.and()
.formLogin()
.loginPage("/login") //跳转登录页面的控制器,该地址要保证和表单提交的地址一致!
.successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest arg0, HttpServletResponse arg1, Authentication arg2)
throws IOException, ServletException {
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal != null && principal instanceof UserDetails) {
UserDetails user = (UserDetails) principal;
System.out.println("loginUser:"+user.getUsername());
//维护在session中
arg0.getSession().setAttribute("userDetail", user);
arg1.sendRedirect("/");
}
}
})
.permitAll()
.and()
.logout()
.permitAll()
.and()
.csrf().disable(); //暂时禁用CSRF,否则无法提交表单
}
/**
* 重写该方法,添加自定义用户
* */
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("admin").password("admin").roles("ADMIN","USER")
.and()
.withUser("terry").password("terry").roles("USER")
.and()
.withUser("larry").password("larry").roles("USER");
}
}
Mybatis Authentication
数据库认证。也就是说要提供数据库的支持,用户信息和角色统一保存到数据库中,这样后期可以提供注册功能向数据库中添加用户信息。
1. 数据库设计
设计了三张表,用户表,角色表,用户角色表,用户与角色之前是多对多关系。外键维护在桥表中。
建表语句如下
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for tbl_role
-- ----------------------------
DROP TABLE IF EXISTS `tbl_role`;
CREATE TABLE `tbl_role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for tbl_user
-- ----------------------------
DROP TABLE IF EXISTS `tbl_user`;
CREATE TABLE `tbl_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(255) DEFAULT NULL,
`password` varchar(255) DEFAULT NULL,
`state` varchar(255) DEFAULT NULL,
`name` varchar(255) DEFAULT NULL,
`gender` varchar(255) DEFAULT NULL,
`birth` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for tbl_user_role
-- ----------------------------
DROP TABLE IF EXISTS `tbl_user_role`;
CREATE TABLE `tbl_user_role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) DEFAULT NULL,
`role_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`),
KEY `role_id` (`role_id`),
CONSTRAINT `tbl_user_role_ibfk_2` FOREIGN KEY (`role_id`) REFERENCES `tbl_role` (`id`),
CONSTRAINT `tbl_user_role_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `tbl_user` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;
2. 提供对应的bean mapper service
都是基础代码,这里就不详细列出来。随后提交到github上
3. 自定义身份验证
3.1 创建UserDetails的实现类
为了使得我们的用户角色类能和security中的能够结合起来,需要重新建一个类MyUserDetails实现UserDetails接口。
MyUserDetails
/**
* 自定义用户身份信息
* */
public class MyUserDetails implements UserDetails {
// 用户信息
private User user;
// 用户角色
private Collection<? extends GrantedAuthority> authorities;
public MyUserDetails(User user, Collection<? extends GrantedAuthority> authorities) {
super();
this.user = user;
this.authorities = authorities;
}
/**
*
*/
private static final long serialVersionUID = 1L;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return this.user.getPassword();
}
@Override
public String getUsername() {
return this.user.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return this.user.getState().equals(User.STATE_ACCOUNTEXPIRED);
}
@Override
public boolean isAccountNonLocked() {
return this.user.getState().equals(User.STATE_LOCK);
}
@Override
public boolean isCredentialsNonExpired() {
return this.user.getState().equals(User.STATE_TOKENEXPIRED);
}
@Override
public boolean isEnabled() {
return this.user.getState().equals(User.STATE_NORMAL);
}
}
用户身份验证 AuthUserDetailService
/**
* 用户身份认证服务类
* */
@Service("userDetailsService")
public class AuthUserDetailService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Autowired
private UserRoleMapper userRoleMapper;
@Override
public UserDetails loadUserByUsername(String name)
throws UsernameNotFoundException {
UserDetails userDetails = null;
try {
User user = userMapper.findByUsername(name);
if(user != null) {
List<UserRole> urs = userRoleMapper.findByUserId(user.getId());
Collection<GrantedAuthority> authorities = new ArrayList<>();
for(UserRole ur : urs) {
String roleName = ur.getRole().getName();
SimpleGrantedAuthority grant = new SimpleGrantedAuthority(roleName);
authorities.add(grant);
}
//封装自定义UserDetails类
userDetails = new MyUserDetails(user, authorities);
} else {
throw new UsernameNotFoundException("该用户不存在!");
}
} catch (Exception e) {
e.printStackTrace();
}
return userDetails;
}
}
自定义认证服务
/**
* 自定义认证服务
* */
@Service("securityProvider")
public class SecurityProvider implements AuthenticationProvider {
private UserDetailsService userDetailsService;
public SecurityProvider(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
@Override
public Authentication authenticate(Authentication authenticate) throws AuthenticationException {
UsernamePasswordAuthenticationToken token
= (UsernamePasswordAuthenticationToken) authenticate;
String username = token.getName();
UserDetails userDetails = null;
if(username !=null) {
userDetails = userDetailsService.loadUserByUsername(username);
}
System.out.println("$$"+userDetails);
if(userDetails == null) {
throw new UsernameNotFoundException("用户名/密码无效");
}
else if (!userDetails.isEnabled()){
System.out.println("jinyong用户已被禁用");
throw new DisabledException("用户已被禁用");
}else if (!userDetails.isAccountNonExpired()) {
System.out.println("guoqi账号已过期");
throw new LockedException("账号已过期");
}else if (!userDetails.isAccountNonLocked()) {
System.out.println("suoding账号已被锁定");
throw new LockedException("账号已被锁定");
}else if (!userDetails.isCredentialsNonExpired()) {
System.out.println("pingzheng凭证已过期");
throw new LockedException("凭证已过期");
}
String password = userDetails.getPassword();
//与authentication里面的credentials相比较
if(!password.equals(token.getCredentials())) {
throw new BadCredentialsException("Invalid username/password");
}
//授权
return new UsernamePasswordAuthenticationToken(userDetails, password,userDetails.getAuthorities());
}
@Override
public boolean supports(Class<?> authentication) {
//返回true后才会执行上面的authenticate方法,这步能确保authentication能正确转换类型
return UsernamePasswordAuthenticationToken.class.equals(authentication);
}
}
核心认证配置
@Configuration
@EnableWebSecurity
public class AuthConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private AuthenticationProvider securityProvider;
@Override
protected UserDetailsService userDetailsService() {
//自定义用户信息类
return this.userDetailsService;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//自定义AuthenticationProvider
auth.authenticationProvider(securityProvider);
}
/**
* 重写该方法,设定用户访问权限
* 用户身份可以访问 订单相关API
* */
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/orders/**").hasRole("USER") //用户权限
.antMatchers("/users/**").hasRole("ADMIN") //管理员权限
.and()
.formLogin()
.loginPage("/login") //跳转登录页面的控制器,该地址要保证和表单提交的地址一致!
//成功处理
.successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest arg0, HttpServletResponse arg1, Authentication arg2)
throws IOException, ServletException {
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal != null && principal instanceof UserDetails) {
UserDetails user = (UserDetails) principal;
System.out.println("loginUser:"+user.getUsername());
//维护在session中
arg0.getSession().setAttribute("userDetail", user);
arg1.sendRedirect("/");
}
}
})
//失败处理
.failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException authenticationException)
throws IOException, ServletException {
System.out.println("error"+authenticationException.getMessage());
response.sendRedirect("/login");
}
})
.permitAll()
.and()
.logout()
.permitAll()
.and()
.csrf().disable(); //暂时禁用CSRF,否则无法提交表单
}
}
这时候就可以准备通过数据库用户进行登录。
用户访问资源-》security拦截-》跳转到login-》提交表单-》securityProvider处理用户信息-》借助UserDetailsService 获取用户信息-》认证成功/失败
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。