18.1 The application senario

  • Add a new workout record for a user
  • Find all the workouts for a user
  • Delete a workout

image.png

18.2 Configuring Keycloak as an authorization server

docker 安装 Keycloak

  1. docker network create keycloak-network
  2. docker run --name mysql -d -p 3306:3306 --net keycloak-network -e MYSQL_DATABASE=keycloak -e MYSQL_USER=keycloak -e MYSQL_PASSWORD=keycloak -e MYSQL_ROOT_PASSWORD=keycloak mysql:5.7
  3. docker run --name keycloak -d -p 8080:8080 --net keycloak-network -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin -e DB_USER=keycloak -e DB_PASSWORD=keycloak -e JDBC_PARAMS='useSSL=false' jboss/keycloak

访问:101.132.251.198:8080
image.png
进入控制台页面会报错:
解决方案:
注意修改

  1. docker run -d -p 8080:8080 -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD='admin1024' -e KEYCLOAK_BIND_ADDRESS=0.0.0.0 -e KEYCLOAK_ENABLE_TLS=false quay.io/keycloak/keycloak:15.0.2'
  2. docker exec -it {contaierID} bash
  3. cd /opt/jboss/keycloak/bin
  4. ./kcadm.sh config credentials --server http://localhost:8080/auth --realm master --user admin
  5. ./kcadm.sh update realms/master -s sslRequired=NONE

image.png
这个配置提供了token endpoint,authorization endpoint还有grand types列表
image.png
image.png
进一步配置authorization server:

  1. 为系统注册一个client
  2. 定义一个client scope
  3. 添加users
  4. 定义user roles以及自定义的访问tokens

    18.2.1 Registering a client for our system

    image.png
    image.png

    18.2.2 Specifying client scopes

    image.png
    image.png
    image.png

    18.2.3 Adding users and obtaining access tokens

    image.png
    image.png
    image.png
    image.png
    调用授权服务的/token端点:
    image.png
    image.png
    image.png
    发送请求之后:
    image.png
    对这个token解码之后发现:roles以及username都没有,所以接下来应该分配roles给users以及自定义jwt去包含所有的数据
    image.png

    18.2.4 Defining the user roles

    创建roles
    image.png
    将roles分配给users
    将mary设置为fitnessadmin其他的为fitnessuser
    image.png
    我们还需要添加三个细节到tokens中:
  • Roles
  • Username
  • Audience claim

image.png
image.png
image.png
全部设置之后,token解码后:
image.png

18.3 Implementing the resource server

依赖:

  1. <dependencies>
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-security</artifactId>
  5. </dependency>
  6. <dependency>
  7. <groupId>org.springframework.boot</groupId>
  8. <artifactId>spring-boot-starter-web</artifactId>
  9. </dependency>
  10. <dependency>
  11. <groupId>org.springframework.cloud</groupId>
  12. <artifactId>spring-cloud-starter-oauth2</artifactId>
  13. </dependency>
  14. <dependency>
  15. <groupId>org.springframework.boot</groupId>
  16. <artifactId>spring-boot-starter-data-jpa</artifactId>
  17. </dependency>
  18. <dependency>
  19. <groupId>org.springframework.security</groupId>
  20. <artifactId>spring-security-data</artifactId>
  21. </dependency>
  22. <dependency>
  23. <groupId>mysql</groupId>
  24. <artifactId>mysql-connector-java</artifactId>
  25. </dependency>
  26. <dependency>
  27. <groupId>org.projectlombok</groupId>
  28. <artifactId>lombok</artifactId>
  29. </dependency>
  30. </dependencies>

导入sql文件
image.png
配置文件:
image.png
定义entity

  1. @Entity
  2. @Data
  3. public class Workout {
  4. @Id
  5. @GeneratedValue(strategy = GenerationType.IDENTITY)
  6. private int id;
  7. private String user;
  8. private LocalDateTime start;
  9. private LocalDateTime end;
  10. private int difficulty;
  11. }

定义repository

  1. public interface WorkoutRepository extends JpaRepository<Workout, Integer> {
  2. @Query("select w from Workout w where w.user = ?#{authentication.name}")
  3. List<Workout> findAllByUser();
  4. }

定义service

  1. @Service
  2. public class WorkoutService {
  3. @Autowired
  4. private WorkoutRepository workoutRepository;
  5. @PreAuthorize("#workout.user == authentication.name")
  6. public void saveWorkout(Workout workout){
  7. workoutRepository.save(workout);
  8. }
  9. public List<Workout> findWorkouts(){
  10. return workoutRepository.findAllByUser();
  11. }
  12. public void deleteWorkout(Integer id){
  13. workoutRepository.deleteById(id);
  14. }
  15. }

定义controller

  1. @RestController
  2. @RequestMapping("/workout")
  3. public class WorkoutController {
  4. @Autowired
  5. private WorkoutService workoutService;
  6. @PostMapping("/")
  7. public void add(@RequestBody Workout workout){
  8. workoutService.saveWorkout(workout);
  9. }
  10. @GetMapping("/")
  11. public List<Workout> findAll(){
  12. return workoutService.findWorkouts();
  13. }
  14. @DeleteMapping("/{id}")
  15. public void delete(@PathVariable Integer id){
  16. workoutService.deleteWorkout(id);
  17. }
  18. }

添加两个自定义配置:
image.png
定义配置类:

  1. @Configuration
  2. @EnableResourceServer
  3. @EnableGlobalMethodSecurity(prePostEnabled = true)
  4. public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
  5. @Value("${claim.aud}")
  6. private String claimAud;
  7. @Value("${jwkSetUri}")
  8. private String urlJwk;
  9. @Override
  10. public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
  11. resources.tokenStore(tokenStore());
  12. resources.resourceId(claimAud);
  13. }
  14. @Bean
  15. public TokenStore tokenStore() {
  16. return new JwkTokenStore(urlJwk);
  17. }
  18. @Override
  19. public void configure(HttpSecurity http) throws Exception {
  20. http.authorizeRequests()
  21. .mvcMatchers(HttpMethod.DELETE, "/**")
  22. .hasAnyAuthority("fitnessadmin")
  23. .anyRequest().authenticated();
  24. }
  25. @Bean
  26. public SecurityEvaluationContextExtension securityEvaluationContextExtension(){
  27. return new SecurityEvaluationContextExtension();
  28. }
  29. }

18.4 Testing the application

18.4.1 Proving an authenticated user can only add a record for themself

  1. {
  2. "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJsQk9DX2VaSTdiTklzVVRadEszTlpaMDUybnpOQUFMb25XMmZ4aFpTb0JZIn0.eyJleHAiOjE2NTM3OTgwNzMsImlhdCI6MTY1Mzc5ODAxMywianRpIjoiYzQ1ZDkwZjctNmE4Mi00NmRjLWJjZjUtMzZiYjM4YTMyMjQ1IiwiaXNzIjoiaHR0cDovLzEwMS4xMzIuMjUxLjE5ODo4MDgwL2F1dGgvcmVhbG1zL21hc3RlciIsImF1ZCI6ImZpdG5lc3NhcHAiLCJzdWIiOiJlOGNlNjkwMC0yMTY5LTRlOTctOGJlOC1hZTI1NGNkNjliNDAiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJmaXRuZXNzYXBwIiwic2Vzc2lvbl9zdGF0ZSI6IjQxMWRiN2M0LTUzMDMtNDNjZS04NmY1LWE1M2NhNWNkMDZiNCIsImFjciI6IjEiLCJzY29wZSI6ImZpdG5lc3NhcHAiLCJzaWQiOiI0MTFkYjdjNC01MzAzLTQzY2UtODZmNS1hNTNjYTVjZDA2YjQiLCJ1c2VyX25hbWUiOiJiaWxsIiwiYXV0aG9yaXRpZXMiOlsiZml0bmVzc3VzZXIiXX0.HEG7B7ICjeSjIixTU4jgx6r4V6jfLFU7l2cXEQIiVlTB1Q8j3ForbIY7QbkBb6w-yp1GbirBUTsfF7RDgK1vHWEe8sgVHIsjWPXcwLIh9DHG63b3h9sbPmPqGU53ZIuxZSwOzhrHn_y9reS0weskoU5fvhWk3YP84Te4vWCxtPVAJ650NG49ZzsND5pMWKQQmLfjm-apB9h9wZW8Zy9eH9aHOjwQvCqvdTaDM6dZu2AHnYwuCYZJe8Pslgyle7rIHJJzgXqguFeiDXBNDCNirTLtuiosaMmZuihhtN_DgXc6UcWE-RLlDAeteT5eoZq7r2eMDBPwu4GT8RN0tBaGfw",
  3. "expires_in": 60,
  4. "refresh_expires_in": 1800,
  5. "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJkNjVhYzIwOC1lMTVhLTRhMGItOWNmMy1hNmExZTA1MDBiOGUifQ.eyJleHAiOjE2NTM3OTk4MTMsImlhdCI6MTY1Mzc5ODAxMywianRpIjoiYjBiYTM1ZTItZDc3MC00YTRjLWI2MGQtNTgwNjdkZGRjYmY1IiwiaXNzIjoiaHR0cDovLzEwMS4xMzIuMjUxLjE5ODo4MDgwL2F1dGgvcmVhbG1zL21hc3RlciIsImF1ZCI6Imh0dHA6Ly8xMDEuMTMyLjI1MS4xOTg6ODA4MC9hdXRoL3JlYWxtcy9tYXN0ZXIiLCJzdWIiOiJlOGNlNjkwMC0yMTY5LTRlOTctOGJlOC1hZTI1NGNkNjliNDAiLCJ0eXAiOiJSZWZyZXNoIiwiYXpwIjoiZml0bmVzc2FwcCIsInNlc3Npb25fc3RhdGUiOiI0MTFkYjdjNC01MzAzLTQzY2UtODZmNS1hNTNjYTVjZDA2YjQiLCJzY29wZSI6ImZpdG5lc3NhcHAiLCJzaWQiOiI0MTFkYjdjNC01MzAzLTQzY2UtODZmNS1hNTNjYTVjZDA2YjQifQ.JMildS3qX1hV2P7jGVr96Zkj3mDL5AcVYAs7PKEEn9M",
  6. "token_type": "Bearer",
  7. "not-before-policy": 0,
  8. "session_state": "411db7c4-5303-43ce-86f5-a53ca5cd06b4",
  9. "scope": "fitnessapp"
  10. }

eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJsQk9DX2VaSTdiTklzVVRadEszTlpaMDUybnpOQUFMb25XMmZ4aFpTb0JZIn0.eyJleHAiOjE2NTM4MDA5NDQsImlhdCI6MTY1MzgwMDg4NCwianRpIjoiM2U5MDQyNGMtMDY4MC00Njg0LTgxNmMtMDFmMjQwMGY0ZTMxIiwiaXNzIjoiaHR0cDovLzEwMS4xMzIuMjUxLjE5ODo4MDgwL2F1dGgvcmVhbG1zL21hc3RlciIsImF1ZCI6ImZpdG5lc3NhcHAiLCJzdWIiOiJlOGNlNjkwMC0yMTY5LTRlOTctOGJlOC1hZTI1NGNkNjliNDAiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJmaXRuZXNzYXBwIiwic2Vzc2lvbl9zdGF0ZSI6IjI3YjY4MjAzLWIwNDYtNDc5ZS05YmZhLWE4NDllZTAxN2Y4ZCIsImFjciI6IjEiLCJzY29wZSI6ImZpdG5lc3NhcHAiLCJzaWQiOiIyN2I2ODIwMy1iMDQ2LTQ3OWUtOWJmYS1hODQ5ZWUwMTdmOGQiLCJ1c2VyX25hbWUiOiJiaWxsIiwiYXV0aG9yaXRpZXMiOlsiZml0bmVzc3VzZXIiXX0.hesAxNDtMcSsbaeCsF430TMRJ8CAlQnjftnG1WA1tt6_RfVVqXr0v8j1y3vPX9ytWYhxmZ8IVphYp2VbpLog1G0ORMroAFPiqqAScX-GmVdPBPF6dkPRLAGUnRpjxwYIxtia-FQIFbSZYP8o-Bg0ZysADlvgzVa6xKMT246NbOQJbAtcE4N8z-jgXX5JtrTqTltuWzXoGDY_P-g-mrPCpsq0bgnSMmzUflK5epACrTpaEvT4hZ_ylODCO5Iq2Ri3fZFtQkzllpDcxgSweuJfSqrRKYr-Ha3xZyiUUplMyNwqamH0CHpREX0Hr2DsUQDAr06j7J_em0ATZqubIQ-RCA