接着上一章节,咱们在这一章种讨论如何在现有的ssm框架中加入security机制,说白了,就是为咱们项目提供身份验证的功能。现有的需求中大多项目都没法脱离登陆注册功能。若是开发时每一个模块提供一个登陆注册功能,整个项目就会臃肿不堪,单点登陆也就应用而生了。至于OAuth2与springBoot的结合咱们在随后章节讨论,这一章节讨论security机制的简单应用。
基于内存的身份认证功能。也就是说身份信息是保存到内存中。这种方式了解为主,在实际开发中使用较少。html
须要的依赖有mysql
- 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中配置热部署的依赖(方便开发)git
配置就绪后启动项目github
> mvn spring-boot:run
哈,是否是有些意外,咱们就没作什么事情,居然具备受权的功能了,这是security默认帮咱们实现的功能,那么用户名密码是什么呢? 用户名默认为user,密码在启动项目的时候会打印到控制台。web
若是咱们直接点击取消,提示未受权异常。spring
刷新页面后进行登陆。输入user/console中密码,出现以下错误,不过这个错误咱们是能理解的,404找不到,说明没有配置服务。sql
在默认受权管理中若是咱们想添加用户改怎么办?若是咱们想自定义登陆页面怎么办?若是咱们想自定义拦截怎么办?数据库
实际上咱们项目之全部具备受权功能,是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"); } }
重启服务进行测试session
当用户名密码输入错误的时候,出现如下界面
当用户名密码输入正确的时候,是能够继续访问服务,因为咱们还么有提供任何服务,全部均会出现404异常。
订单控制器 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"); } }
重启服务进行登陆
默认状况下,当用户没有登陆就去访问受保护资源时,系统会默认请求/login(get方式),这时重定向到登陆页(spring security自带)。当输入用户名密码点击登陆按钮的时候,系统会请求/login(post方式)。如今咱们但愿自定义登陆页面(默认的登陆页面很丑),可是身份校验仍是但愿由security来进行。这时候咱们只须要将登陆页面重定向到咱们自定义页面便可,这时候DIY表单,可是在这里切记一点。登陆页面重定向的地址和表单提交的地址务必一致!
在原来的基础上扩展了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方式),而后通过咱们这个控制器跳转到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"; } }
注意:这里表单的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中,至此完成自定义登陆页面设置
这里我只是简单处理了一下,经过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"); } }
数据库认证。也就是说要提供数据库的支持,用户信息和角色统一保存到数据库中,这样后期能够提供注册功能向数据库中添加用户信息。
设计了三张表,用户表,角色表,用户角色表,用户与角色以前是多对多关系。外键维护在桥表中。
建表语句以下
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;
都是基础代码,这里就不详细列出来。随后提交到github上
为了使得咱们的用户角色类能和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 获取用户信息-》认证成功/失败