關於 Spring Security Web系統的認證和權限模塊也算是一個系統的基礎設施了,幾乎任何的互聯網服務都會涉及到這方面的要求。在Java EE領域,成熟的安全框架解決方案通常有 Apache Shiro、Spring Security等兩種技術選型。Apache Shiro簡單易用也算是一大優點,但其功能仍是遠不如 Spring Security強大。Spring Security能夠爲 Spring 應用提供聲明式的安全訪問控制,起經過提供一系列能夠在 Spring應用上下文中可配置的Bean,並利用 Spring IoC和 AOP等功能特性來爲應用系統提供聲明式的安全訪問控制功能,減小了諸多重複工做。java
關於JWT JSON Web Token (JWT),是在網絡應用間傳遞信息的一種基於 JSON的開放標準((RFC 7519),用於做爲JSON對象在不一樣系統之間進行安全地信息傳輸。主要使用場景通常是用來在 身份提供者和服務提供者間傳遞被認證的用戶身份信息。關於JWT的科普,能夠看看阮一峯老師的《JSON Web Token 入門教程》。mysql
本文則結合 Spring Security和 JWT兩大利器來打造一個簡易的權限系統。git
本文實驗環境以下:github
2.0.6.RELEASE
IntelliJ IDEA 2018.2.4
另外本文實驗代碼置於文尾,須要自取。web
可 長按 或 掃描 下面的 當心心 來訂閱做者公衆號 CodeSheep,獲取更多 務實、能看懂、可復現的 原創文 ↓↓↓spring
本文實驗爲了簡化考慮,準備作以下設計:sql
role
,包括角色ID
和角色名稱
user
,包括用戶ID
,用戶名
,密碼
user_roles
一個用戶能夠擁有多個角色
pom.xml
中引入 Spring Security和 JWT所必需的依賴<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
複製代碼
server.port=9991
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://121.196.XXX.XXX:3306/spring_security_jwt?useUnicode=true&characterEncoding=utf-8
spring.datasource.username=root
spring.datasource.password=XXXXXX
logging.level.org.springframework.security=info
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jackson.serialization.indent_output=true
複製代碼
用戶實體 User:數據庫
/** * @ www.codesheep.cn * 20190312 */
@Entity
public class User implements UserDetails {
@Id
@GeneratedValue
private Long id;
private String username;
private String password;
@ManyToMany(cascade = {CascadeType.REFRESH},fetch = FetchType.EAGER)
private List<Role> roles;
...
// 下面爲實現UserDetails而須要的重寫方法!
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> authorities = new ArrayList<>();
for (Role role : roles) {
authorities.add( new SimpleGrantedAuthority( role.getName() ) );
}
return authorities;
}
...
}
複製代碼
此處所建立的 User類繼承了 Spring Security的 UserDetails接口,從而成爲了一個符合 Security安全的用戶,即經過繼承 UserDetails,便可實現 Security中相關的安全功能。json
角色實體 Role:安全
/** * @ www.codesheep.cn * 20190312 */
@Entity
public class Role {
@Id
@GeneratedValue
private Long id;
private String name;
... // 省略 getter和 setter
}
複製代碼
主要用於對 JWT Token進行各項操做,好比生成Token、驗證Token、刷新Token等
/** * @ www.codesheep.cn * 20190312 */
@Component
public class JwtTokenUtil implements Serializable {
private static final long serialVersionUID = -5625635588908941275L;
private static final String CLAIM_KEY_USERNAME = "sub";
private static final String CLAIM_KEY_CREATED = "created";
public String generateToken(UserDetails userDetails) {
...
}
String generateToken(Map<String, Object> claims) {
...
}
public String refreshToken(String token) {
...
}
public Boolean validateToken(String token, UserDetails userDetails) {
...
}
... // 省略部分工具函數
}
複製代碼
/** * @ www.codesheep.cn * 20190312 */
@Component
public class JwtTokenFilter extends OncePerRequestFilter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Override
protected void doFilterInternal ( HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
String authHeader = request.getHeader( Const.HEADER_STRING );
if (authHeader != null && authHeader.startsWith( Const.TOKEN_PREFIX )) {
final String authToken = authHeader.substring( Const.TOKEN_PREFIX.length() );
String username = jwtTokenUtil.getUsernameFromToken(authToken);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.validateToken(authToken, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(
request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
chain.doFilter(request, response);
}
}
複製代碼
主要包括用戶登陸和註冊兩個主要的業務
public interface AuthService {
User register( User userToAdd );
String login( String username, String password );
}
複製代碼
/** * @ www.codesheep.cn * 20190312 */
@Service
public class AuthServiceImpl implements AuthService {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private UserRepository userRepository;
// 登陸
@Override
public String login( String username, String password ) {
UsernamePasswordAuthenticationToken upToken = new UsernamePasswordAuthenticationToken( username, password );
final Authentication authentication = authenticationManager.authenticate(upToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
final UserDetails userDetails = userDetailsService.loadUserByUsername( username );
final String token = jwtTokenUtil.generateToken(userDetails);
return token;
}
// 註冊
@Override
public User register( User userToAdd ) {
final String username = userToAdd.getUsername();
if( userRepository.findByUsername(username)!=null ) {
return null;
}
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
final String rawPassword = userToAdd.getPassword();
userToAdd.setPassword( encoder.encode(rawPassword) );
return userRepository.save(userToAdd);
}
}
複製代碼
這是一個高度綜合的配置類,主要是經過重寫 WebSecurityConfigurerAdapter
的部分 configure
配置,來實現用戶自定義的部分。
/** * @ www.codesheep.cn * 20190312 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserService userService;
@Bean
public JwtTokenFilter authenticationTokenFilterBean() throws Exception {
return new JwtTokenFilter();
}
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure( AuthenticationManagerBuilder auth ) throws Exception {
auth.userDetailsService( userService ).passwordEncoder( new BCryptPasswordEncoder() );
}
@Override
protected void configure( HttpSecurity httpSecurity ) throws Exception {
httpSecurity.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll() // OPTIONS請求所有放行
.antMatchers(HttpMethod.POST, "/authentication/**").permitAll() //登陸和註冊的接口放行,其餘接口所有接受驗證
.antMatchers(HttpMethod.POST).authenticated()
.antMatchers(HttpMethod.PUT).authenticated()
.antMatchers(HttpMethod.DELETE).authenticated()
.antMatchers(HttpMethod.GET).authenticated();
// 使用前文自定義的 Token過濾器
httpSecurity
.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
httpSecurity.headers().cacheControl();
}
}
複製代碼
登陸和註冊的 Controller:
/**
* @ www.codesheep.cn
* 20190312
*/
@RestController
public class JwtAuthController {
@Autowired
private AuthService authService;
// 登陸
@RequestMapping(value = "/authentication/login", method = RequestMethod.POST)
public String createToken( String username,String password ) throws AuthenticationException {
return authService.login( username, password ); // 登陸成功會返回JWT Token給用戶
}
// 註冊
@RequestMapping(value = "/authentication/register", method = RequestMethod.POST)
public User register( @RequestBody User addedUser ) throws AuthenticationException {
return authService.register(addedUser);
}
}
複製代碼
再編寫一個測試權限的 Controller:
/**
* @ www.codesheep.cn
* 20190312
*/
@RestController
public class TestController {
// 測試普通權限
@PreAuthorize("hasAuthority('ROLE_NORMAL')")
@RequestMapping( value="/normal/test", method = RequestMethod.GET )
public String test1() {
return "ROLE_NORMAL /normal/test接口調用成功!";
}
// 測試管理員權限
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
@RequestMapping( value = "/admin/test", method = RequestMethod.GET )
public String test2() {
return "ROLE_ADMIN /admin/test接口調用成功!";
}
}
複製代碼
這裏給出兩個測試接口用於測試權限相關問題,其中接口 /normal/test
須要用戶具有普通角色(ROLE_NORMAL
)便可訪問,而接口/admin/test
則須要用戶具有管理員角色(ROLE_ADMIN
)才能夠訪問。
接下來啓動工程,實驗測試看看效果
在文章開頭咱們即在用戶表 user
中插入了一條用戶名爲 codesheep
的記錄,並在用戶-角色表 user_roles
中給用戶 codesheep
分配了普通角色(ROLE_NORMAL
)和管理員角色(ROLE_ADMIN
)
接下來進行用戶登陸,並得到後臺向用戶頒發的JWT Token
不帶 Token直接訪問須要普通角色(ROLE_NORMAL
)的接口 /normal/test
會直接提示訪問不通:
而帶 Token訪問須要普通角色(ROLE_NORMAL
)的接口 /normal/test
纔會調用成功:
同理因爲目前用戶具有管理員角色,所以訪問須要管理員角色(ROLE_ADMIN
)的接口 /admin/test
也能成功:
接下里咱們從用戶-角色表裏將用戶codesheep
的管理員權限刪除掉,再訪問接口 /admin/test
,會發現因爲沒有權限,訪問被拒絕了:
通過一系列的實驗過程,也達到了咱們的預期!
本文涉及的東西仍是蠻多的,最後咱們也將本文的實驗源碼放在 Github上,須要的能夠自取:源碼下載地址
因爲能力有限,如有錯誤或者不當之處,還請你們批評指正,一塊兒學習交流!