SpringSecurity許可權管理系統實戰—五、整合SpringSecurity(下)

codermy發表於2020-08-19

目錄

SpringSecurity許可權管理系統實戰—一、專案簡介和開發環境準備
SpringSecurity許可權管理系統實戰—二、日誌、介面文件等實現
SpringSecurity許可權管理系統實戰—三、主要頁面及介面實現
SpringSecurity許可權管理系統實戰—四、整合SpringSecurity(上)
SpringSecurity許可權管理系統實戰—五、整合SpringSecurity(下)
SpringSecurity許可權管理系統實戰—六、SpringSecurity整合jwt
SpringSecurity許可權管理系統實戰—七、處理一些問題
SpringSecurity許可權管理系統實戰—八、AOP 記錄使用者日誌、異常日誌

前言

上篇文章SpringSecurity整合了一半,這次把另一半整完,所以本篇的序號接著上一篇。

七、自定義使用者資訊

前面我們登入都是用的指定的使用者名稱和密碼或者是springsecurity預設的使用者名稱和列印出來的密碼。我們要想連線上自定義資料庫只需要實現一個自定義的UserDetailsService。

我們新建一個JwtUserDto繼承UserDetails並實現它的方法

@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實現UserDetailsService

@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。如果修改了又刪除掉了,它就會是空值。

在這裡插入圖片描述

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的事就要被拿出來遛一遛。

那麼為什麼密碼加密怎麼重要??因為在你的資料庫被攻擊洩露了資料時,如果你的密碼也被黑客掌握,那麼即使你修復好了資料庫洩露的問題,黑客手上仍然還有著使用者的密碼(總不能要求所有使用者修改密碼吧)

所以我們需要在系統開發之初就儘量的避免這種問題。

那麼說了這麼多,怎麼來加密呢?

其實在SpringSecurity種已經內建了密碼的加密機制,只需要實現一個PasswordEncoder介面即可。

來看一下原始碼

public interface PasswordEncoder {
    String encode(CharSequence var1);

    boolean matches(CharSequence var1, String var2);

    default boolean upgradeEncoding(String encodedPassword) {
        return false;
    }
}

  • encode():把引數按照特定的解析規則進行解析。
  • matches()驗證從儲存中獲取的編碼密碼與編碼後提交的原始密碼是否匹配。如果密碼匹配,則返回 true;如果不匹配,則返回 false。
  • upgradeEncoding():如果解析的密碼能夠再次進行解析且達到更安全的結果則返回 true,否則返回 false。預設返回 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實現無狀態登入

本系列giteegithub中同步更新

相關文章