SpringSecurity許可權管理系統實戰—五、整合SpringSecurity(下)
- 2020 年 8 月 19 日
- 筆記
- SpringSecurity, 後端
系列目錄
前言
上篇文章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實現無狀態登錄