【本文版權歸微信公衆號"代碼藝術"(ID:onblog)全部,如果轉載請務必保留本段原創聲明,違者必究。如果文章有不足之處,歡迎關注微信公衆號私信與我進行交流!】
- 鼓搗了兩天的Spring Security,踩了很多坑。若是你在學Spring Security,剛好又是使用的Spring Boot,那麼給我點個贊吧!這篇博客將會讓你瞭解Spring Security的各類坑!
- 閱讀前說一下,這篇博客是我一字一字打出來的,轉載務必註明出處哦!
- 另外,本文已受權微信公衆號「後端技術精選」獨家發佈
<!-- Web工程 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 數據庫相關 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!-- security 核心 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- thymeleaf 模板--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <!-- 能夠在HTML使用sec標籤操做Security --> <dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity4</artifactId> </dependency><br />
package cn.zyzpp.security.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.UserDetailsService; /** * Create by yster@foxmail.com 2018/6/10/010 18:07 */ @EnableWebSecurity public class MySerurityConfig extends WebSecurityConfigurerAdapter { /*本身實現下面兩個接口*/ @Autowired private AuthenticationProvider authenticationProvider; @Autowired private UserDetailsService userDetailsService; @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/", "/signIn").permitAll()//全部人均可以訪問 .antMatchers("/leve/1").hasRole("VIP1") //設置訪問角色 .antMatchers("/leve/2").hasRole("VIP2") .antMatchers("/leve/3").hasAuthority("VIP2")//設置訪問權限 .anyRequest().authenticated() //其餘全部資源都須要認證,登錄後訪問 .and() .formLogin()//開啓自動配置的受權功能 .loginPage("/login") //自定義登陸頁(controller層須要聲明) .usernameParameter("username") //自定義用戶名name值 .passwordParameter("password") //自定義密碼name值 .failureUrl("/login?error") //登陸失敗則重定向到此URl .permitAll() //登陸頁均可以訪問 .and() .logout()//開啓自動配置的註銷功能 .logoutSuccessUrl("/")//註銷成功後返回到頁面並清空Session .and() .rememberMe() .rememberMeParameter("remember")//自定義rememberMe的name值,默認remember-Me .tokenValiditySeconds(604800);//記住個人時間/秒 } /*定義認證規則*/ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { /* 保存用戶信息到內存中 auth.inMemoryAuthentication() .withUser("張三").password("123456").roles("VIP1") .and() .withUser("李四").password("123456").roles("VIP2"); */ /*自定義認證*/ auth.authenticationProvider(authenticationProvider); auth.userDetailsService(userDetailsService);//不定義的話rememberMe報錯 } /*忽略靜態資源*/ @Override public void configure(WebSecurity web) { web.ignoring().antMatchers("/resources/static/**"); } }
@Bean public DaoAuthenticationProvider daoAuthenticationProvider(){ DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider(); daoAuthenticationProvider.setUserDetailsService(userDetailsService);//獲取用戶信息 daoAuthenticationProvider.setPasswordEncoder(new Md5PasswordEncoder());//MD5加密 daoAuthenticationProvider.setSaltSource(new SaltSource() { //加鹽 @Override public Object getSalt(UserDetails user) { return user.getUsername(); } }); return daoAuthenticationProvider; }
auth.authenticationProvider(authenticationProvider);
Bad credentials
這樣的消息提示你會須要?package cn.zyzpp.security.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.DisabledException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.encoding.Md5PasswordEncoder; import org.springframework.security.core.Authentication; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Component; /** * Create by yster@foxmail.com 2018/6/21/021 15:53 * Authentication 是一個接口,用來表示用戶認證信息的 */ @Component public class MyAuthenticationProvider implements AuthenticationProvider{ @Autowired private MyUserDetailsService userDetailsService; @Override public Authentication authenticate(Authentication authentication){ //1.獲取用戶輸入的用戶名 密碼 String username = authentication.getName(); String password = (String) authentication.getCredentials(); //2.關於MD5加密: //由於咱們是自定義Authentication,因此必須手動加密加鹽而不須要再配置。 password = new Md5PasswordEncoder().encodePassword(password,username); //3.由輸入的用戶名查找該用戶信息,內部拋出異常 UserDetails user = userDetailsService.loadUserByUsername(username); //4.密碼校驗 if (!password.equals(user.getPassword())) { throw new DisabledException("---->UserName :" + username + " password error!"); } return new UsernamePasswordAuthenticationToken(user, password, user.getAuthorities()); } @Override public boolean supports(Class<?> aClass) { return (UsernamePasswordAuthenticationToken.class .isAssignableFrom(aClass)); } }
UsernameNotFoundException
用戶找不到,BadCredentialsException
壞的憑據,但這兩個類都是繼承自AuthenticationException
抽象類,當你拋出這倆異常時,Security底層會捕捉到你拋出的異常,如圖: AuthenticationException
異常並不會被拋出,debug調式一下,你就會感覺到它的曲折歷程,至關感人!而後莫名其妙的被換掉了,並且無解。AccountStatusException
異常被直接拋出了,這正是咱們須要的;有的同窗可能想到了自定義異常,但咱們是結合Security框架,要按人家的規則來,不信你試試。<span class="hljs-comment">/* AuthenticationException經常使用的的子類:(會被底層換掉,不推薦使用) UsernameNotFoundException 用戶找不到 BadCredentialsException 壞的憑據 AccountStatusException用戶狀態異常它包含以下子類:(推薦使用) AccountExpiredException 帳戶過時 LockedException 帳戶鎖定 DisabledException 帳戶不可用 CredentialsExpiredException 證書過時 /</span>
package cn.zyzpp.security.config; import cn.zyzpp.security.entity.Role; import cn.zyzpp.security.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.DisabledException; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.List; /** * 進行認證的時候須要一個 UserDetailsService 來獲取用戶的信息 UserDetails, * 其中包括用戶名、密碼和所擁有的權限等。 * Create by yster@foxmail.com 2018/6/21/021 15:56 */ @Component public class MyUserDetailsService implements UserDetailsService { @Autowired private UserService userService; /* * 採坑筆記: * new SimpleGrantedAuthority("...")時 * 加前戳是Role,經過hasRole()獲取,用來認證角色; * 不加前戳是Authoritiy,經過hasAuthority()獲取,用來鑑定權限; * 總結:加前戳是角色,不加前戳是權限。此前戳只用於本類。 */ String role_ = "ROLE_"; @Override public UserDetails loadUserByUsername(String username) { //1.業務層根據username獲取該用戶 cn.zyzpp.security.entity.User user = userService.findUserByUserName(username); if (user == null) { //這裏咱們不拋出UsernameNotFoundException由於Security會把咱們拋出的該異常捕捉並換掉; //這裏要明確Security拋出的異常沒法被ControllerAdvice捕捉到,沒法進行統一異常處理; //而咱們只須要打印正確的異常消息便可,Security自動把異常添加到HttpServletRequest或HttpSession中 throw new DisabledException("---->UserName :" + username + " not found!"); } //2.從業務層獲取用戶權限並轉爲Authorities List<GrantedAuthority> authorities = new ArrayList<>(); for (Role role : user.getRoleList()) { authorities.add(new SimpleGrantedAuthority(role.getName()));//設置權限 authorities.add(new SimpleGrantedAuthority(role_ + role.getName()));//設置角色 } //3.返回Spring定義的User對象 return new User(username, user.getPassword(), authorities); } }
【本文版權歸微信公衆號"代碼藝術"(ID:onblog)全部,如果轉載請務必保留本段原創聲明,違者必究。如果文章有不足之處,歡迎關注微信公衆號私信與我進行交流!】
auth.inMemoryAuthentication() .withUser("張三") .password("123456") .roles("ROLE_VIP1") .authorities("VIP1")
authorities.add(new SimpleGrantedAuthority("權限名"));
方法時,當Role前綴爲」ROLE_」(默認)時將使用ROLE_ADMIN角色。html
.roles("ROLE_VIP1")
方法註冊Role時,先經過role.startsWith("ROLE_")
斷言輸入的角色名是不是"ROLE_"
開頭的,若是不是,補充"RELE_"
前戳。"ROLE_"
前戳,該默認前戳也是能夠本身修改的。package cn.zyzpp.security.controller; import cn.zyzpp.security.service.UserService; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; /** * Create by yster@foxmail.com 2018/6/10/010 18:35 */ @Controller public class MyController { @Autowired UserService userService; @Autowired HttpSession session; @Autowired HttpServletRequest request; /*ModelMap的Key*/ final String ERROR = "error"; /** * 自定義登陸頁並進行異常信息提示 * 須要在Security中設置 */ @RequestMapping(value = "/login") public String login(ModelMap modelMap){ /* security的AuthenticationException異常自動保存在request或session中 官方默認保存在Session,但咱們自定義過多。我測試是在request中。 因此在html頁面還須要搭配th:if="${param.error!=null}"檢查Url是否有參數error */ String key = WebAttributes.AUTHENTICATION_EXCEPTION; if (session.getAttribute(key)!=null){ // System.out.println("request"); AuthenticationException exception = (AuthenticationException) session.getAttribute(key); modelMap.addAttribute(ERROR,exception.getMessage()); } if (request.getAttribute(key)!=null){ // System.out.println("session"); AuthenticationException exception = (AuthenticationException) request.getAttribute(key); modelMap.addAttribute(ERROR,exception.getMessage()); } return "login"; } }
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"/> <title>登陸頁面</title> </head> <body> <form th:action="@{/login}" method="post"> 用戶名:<input type="text" placeholder="username" name="username" required=""/><br/> 密碼:<input type="password" placeholder="password" name="password" required=""/><br/> 記住我:<input type="checkbox" name="remember"/> <input type="submit" value="提交"/> <span th:if="${param.error!=null}" th:text="${error}"/> </form> </body> </html>
SimpleUrlAuthenticationFailureHandler
(簡單認證故障處理)會把異常保存到request
或session
中,forwardToDestination
默認爲false
,也就是保存在session
,實際咱們測試是保存在request
。<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4"> <head> <meta charset="UTF-8"/> <title>首頁</title> </head> <body> <div sec:authorize="isAuthenticated()"> <form th:action="@{/logout}" method="POST"> <input type="submit" value="註銷" /> </form> user:<b sec:authentication="name"></b><br/> <!-- principal對應org.springframework.security.core.userdetails.User類 --> Role:<b sec:authentication="principal.authorities"></b> </div> <div sec:authorize="!isAuthenticated()"> <h2>遊客你好!</h2>請<a th:href="@{/login}">登陸</a> </div> <div sec:authorize="hasRole('VIP1')"> <h2>ROLE_VIP1_可見</h2> </div> <div sec:authorize="hasRole('VIP2')"> <h2>ROLE_VIP2_可見</h2> </div> <div sec:authorize="hasAuthority('VIP1')"> <h2>Authority:VIP1_可見</h2> </div> </body> </html>
/** * 不使用sec標籤(不推薦) * 在Controller獲取用戶信息 */ @RequestMapping("/index") public String index1(ModelMap model){ userAndRoles(model); return "index"; } /** * Security輔助方法:獲取用戶信息 */ private void userAndRoles(ModelMap model) { //從Security獲取當前用戶會話 Object principal = SecurityContextHolder.getContext() .getAuthentication() .getPrincipal(); User user = null; //判斷用戶已經登陸 if (principal instanceof User){ user = (User) principal; //遍歷迭代器獲取用戶權限 Iterator<GrantedAuthority> iterator = user.getAuthorities().iterator(); List<String> roles = new ArrayList<>(); while (iterator.hasNext()){ roles.add(iterator.next().getAuthority()); } //保存角色信息 model.addAttribute("roles",roles.toString()); } //保存用戶信息,未登陸爲空 model.addAttribute("user",user); }
/** * 權限表 * Create by yster@foxmail.com 2018/6/21/021 18:00 */ @Entity @Table(name = "role") public class Role { @Id @GeneratedValue private int id; private String name; ... }
/** * Create by yster@foxmail.com 2018/6/21/021 17:59 */ @Entity @Table(name = "user",uniqueConstraints = {@UniqueConstraint(columnNames="username")}) public class User { @Id @GeneratedValue private int id; private String username; private String password; @OneToMany(cascade={CascadeType.ALL}, fetch=FetchType.EAGER) @JoinColumn(name = "r_id") private List<Role> roleList; .... }
- 關於Security的部分先到這裏,之因此寫這篇博客,源於網上的相關資料略少,坑略多,畢竟作伸手黨作慣了,一些坑踩的仍是不容易的!
Spring Security從2.0版本開始,提供了方法級別的安全支持,並提供了 JSR-250 的支持。寫一個配置類 SecurityConfig 繼承 WebSecurityConfigurationAdapter,並加上相關注解,就能夠開啓方法級別的保護。java
@EnableWebSecurity @Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) public class SerurityConfig extends WebSecurityConfigurerAdapter { }
在上面的配置代碼中,@EnableGlobalMethodSecurity(prePostEnabled = true) 註解開啓了方法級別的保護,括號後面的參數可選,可選的參數以下。mysql
通常來講,只會用到 prePostEnabled。由於 即 @PreAuthorize 註解比 @PostAuthorize 註解更適合方法級別的安全控制,而且支持 Spring EL 表達式,適合 Spring 開發者。其中,@PreAuthorize 註解會在進入方法錢進行權限驗證,@PostAuthorize 註解在方法執行後再進行權限驗證。web
如何在方法上寫權限註解呢?
例若有權限點字符串「ROLE_ADMIN」,在方法上能夠寫爲 @PreAuthorize(「hasRole(‘ADMIN’)」),也能夠寫爲 @PreAuthorize(「hasAuthority(‘ROLE_ADMIN’)」),這兩者是等價的。加多個權限點,能夠寫爲 @PreAuthorize(「hasRole(‘ADMIN’,‘USER’)」)、@PreAuthorize(「hasAuthority(‘ROLE_ADMIN’,‘ROLE_USER’)」)。spring
【本文版權歸微信公衆號"代碼藝術"(ID:onblog)全部,如果轉載請務必保留本段原創聲明,違者必究。如果文章有不足之處,歡迎關注微信公衆號私信與我進行交流!】sql