SpringSecurity權限管理系統實戰—1、項目簡介和開發環境準備
SpringSecurity權限管理系統實戰—2、日誌、接口文檔等實現
SpringSecurity權限管理系統實戰—3、主要頁面及接口實現
SpringSecurity權限管理系統實戰—4、整合SpringSecurity(上)
SpringSecurity權限管理系統實戰—5、整合SpringSecurity(下)
SpringSecurity權限管理系統實戰—6、SpringSecurity整合jwt
SpringSecurity權限管理系統實戰—7、處理一些問題
SpringSecurity權限管理系統實戰—8、AOP 記錄用戶日誌、異常日誌html
上篇文章SpringSecurity整合了一半,此次把另外一半整完,因此本篇的序號接着上一篇。前端
前面咱們登陸都是用的指定的用戶名和密碼或者是springsecurity默認的用戶名和打印出來的密碼。咱們要想鏈接上自定義數據庫只須要實現一個自定義的UserDetailsService。java
咱們新建一個JwtUserDto繼承UserDetails並實現它的方法git
@Data @AllArgsConstructor public class JwtUserDto implements UserDetails { //用戶數據 private MyUser myUser; //用戶權限的集合 @JsonIgnore private List<GrantedAuthority> authorities; public List<String> getRoles() { return authorities.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList()); } //加密後的密碼 @Override public String getPassword() { return myUser.getPassword(); } //用戶名 @Override public String getUsername() { return myUser.getUserName(); } //是否過時 @Override public boolean isAccountNonExpired() { return true; } //是否鎖定 @Override public boolean isAccountNonLocked() { return true; } //憑證是否過時 @Override public boolean isCredentialsNonExpired() { return true; } //是否可用 @Override public boolean isEnabled() { return myUser.getStatus() == 1 ? true : false; } }
自定義一個UserDetailsServiceImpl實現UserDetailsServicegithub
@Service @Slf4j public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private UserService userService; @Autowired private MenuDao menuDao; @Override public JwtUserDto loadUserByUsername(String userName) throws UsernameNotFoundException { MyUser user = userService.getUser(userName);//根據用戶名獲取用戶 if (user == null ){ throw new UsernameNotFoundException("用戶名不存在");//這個異常必定要拋 }else if (user.getStatus().equals(MyUser.Status.LOCKED)) { throw new LockedException("用戶被鎖定,請聯繫管理員"); } List<GrantedAuthority> grantedAuthorities = new ArrayList<>(); List<MenuIndexDto> list = menuDao.listByUserId(user.getId()); List<String> collect = list.stream().map(MenuIndexDto::getPermission).collect(Collectors.toList()); for (String authority : collect){ if (!("").equals(authority) & authority !=null){ GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(authority); grantedAuthorities.add(grantedAuthority); } }//將用戶所擁有的權限加入GrantedAuthority集合中 JwtUserDto loginUser =new JwtUserDto(user,grantedAuthorities); return loginUser; } }
這裏在獲取權限的時候遇到了個小小的坑,就是mybatis數據裏的空值和null,在你從未對這個數據修改時,它就是null。若是修改了又刪除掉了,它就會是空值。spring
meudao中的listByUserId方法數據庫
@Select("SELECT DISTINCT sp.id,sp.parent_id,sp.name,sp.icon,sp.url,sp.type,sp.permission " + "FROM my_role_user sru " + "INNER JOIN my_role_menu srp ON srp.role_id = sru.role_id " + "LEFT JOIN my_menu sp ON srp.menu_id = sp.id " + "WHERE " + "sru.user_id = #{userId}") @Result(property = "title",column = "name") @Result(property = "href",column = "url") List<MenuIndexDto> listByUserId(@Param("userId")Integer userId);
老話題來聊一聊,加密的重要性。安全
2011年國內某開發者社區(可不就是csdn嗎)被攻擊數據庫,600多萬明文存儲的用戶帳號被公開,大量用戶隱私泄露。服務器
這是個老梗了,幾乎每篇說加密重要性的博文中,csdn的事就要被拿出來遛一遛。session
那麼爲何密碼加密怎麼重要??由於在你的數據庫被攻擊泄露了數據時,若是你的密碼也被黑客掌握,那麼即便你修復好了數據庫泄露的問題,黑客手上仍然還有着用戶的密碼(總不能要求全部用戶修改密碼吧)
因此咱們須要在系統開發之初就儘可能的避免這種問題。
那麼說了這麼多,怎麼來加密呢?
其實在SpringSecurity種已經內置了密碼的加密機制,只須要實現一個PasswordEncoder接口便可。
來看一下源碼
public interface PasswordEncoder { String encode(CharSequence var1); boolean matches(CharSequence var1, String var2); default boolean upgradeEncoding(String encodedPassword) { return false; } }
第一個參數表示須要被解析的密碼。第二個參數表示存儲的密碼。
Spring Security 還內置了幾種經常使用的 PasswordEncoder 接口,官方推薦使用的是BCryptPasswordEncoder
。咱們來配置一下。在SpringConfig種添加以下代碼。
@Autowired private UserDetailsService userDetailsService; @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); }//自定義userDetailsService加密
是否是十分簡單,咱們再重啓項目,這時候控制檯就再也不打印密碼,如今須要輸入數據庫中的用戶名密碼才能登陸。
以前咱們在繪製菜單時,把用戶的id給寫死了。如今咱們要從SpringSecurity中來獲取用戶信息。
有兩種方法獲取已登陸用戶的信息,一種是從session中拿,另外一種就是SpringSecurity提供的方法。這裏選擇後一種方法。
咱們能夠經過如下方法來獲取登陸後用戶的信息(其他還有獲取登陸ip等方法,很少介紹)
SecurityContextHolder.getContext().getAuthentication().getPrincipal()
咱們轉換下類型
JwtUserDto jwtUserDto = (JwtUserDto)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
打印一下jwtUserDto,看到咱們確實拿到了用戶的信息
那麼咱們改寫下經過用戶id獲取菜單這個方法
@GetMapping(value = "/index") @ResponseBody @ApiOperation(value = "經過用戶id獲取菜單") public List<MenuIndexDto> getMenu() { JwtUserDto jwtUserDto = (JwtUserDto)SecurityContextHolder.getContext().getAuthentication().getPrincipal(); Integer userId = jwtUserDto.getMyUser().getId(); return menuService.getMenu(userId); }
在將前端寫死的userId刪除。如今咱們已經能根據登陸用戶的不一樣來自動繪製菜單了。
擁有admin權限的用戶
普通權限的用戶
咱們目前只是繪製出了不一樣權限用戶能操做的界面,可是尚未真正的進行權限控制。
以前在七中,咱們已經將每一個用戶所擁有的權限集合放入了GrantedAuthority集合中
在以前打印的用戶信息中能夠看到 authorities中就是該用戶所擁有的權限
SpringSecurity會自動幫咱們進行權限控制。而咱們要作的就是在須要進行權限控制的方法上添加上權限標識便可。
例如:用戶管理的權限標識是user:list
咱們只須要在相關的接口上加上@PreAuthorize("hasAnyAuthority('user:list')")便可
@GetMapping("/index") @PreAuthorize("hasAnyAuthority('user:list')") public String index(){ return "system/user/user"; } @GetMapping @ResponseBody @ApiOperation(value = "用戶列表") @PreAuthorize("hasAnyAuthority('user:list')") public Result<MyUser> userList(PageTableRequest pageTableRequest, UserQueryDto userQueryDto){ pageTableRequest.countOffset(); return userService.getAllUsersByPage(pageTableRequest.getOffset(),pageTableRequest.getLimit(),userQueryDto); }
如今咱們登陸普通用戶來操做相關接口,發現報錯
控制檯打印
修改全部接口,在須要權限控制的接口上添加註解
雖然說如今功能已經實現了,用戶雖然說不能訪問沒有權限的功能了,可是異常沒有處理。若是點擊,若是前端也沒有作錯誤的攔截的話,用戶會看到一串的報錯信息,這很不友好,而且也會對服務器形成壓力。
咱們只須要在以前建立的全局異常處理類中捕獲上圖的異常便可。
@ExceptionHandler(AccessDeniedException.class) public Result handleAuthorizationException(AccessDeniedException e) { log.error(e.getMessage()); return Result.error().code(ResultCode.FORBIDDEN).message("沒有權限,請聯繫管理員受權"); }
重啓項目,在前端書寫相應規則,就會十分友好
其實SpringSecurity默認註冊了一個/logout路由,經過這個路由能夠註銷登陸狀態,包括Session和remember-me等等。
咱們能夠直接在SpringSecurityConfig的configure中定義相應規則,相似formlogin。也能夠自定義一個LogoutHadnler,具體能夠看這篇文章
至此SpringSecurity的一些經常使用功能已經實現,下一節咱們整合jwt實現無狀態登陸