Skip to content

Commit 38bec62

Browse files
committed
add spring security
1 parent 723ff2a commit 38bec62

12 files changed

+181
-12
lines changed

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,6 @@ show: pack
2121

2222
build: pack
2323
./gradlew bootJar
24+
25+
check: lint
26+
cd src/main/client && npm run test

build.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ repositories {
1313
dependencies {
1414
implementation 'org.springframework.boot:spring-boot-starter-websocket'
1515
implementation 'com.auth0:java-jwt:4.4.0'
16+
implementation 'org.springframework.boot:spring-boot-starter-security'
1617
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
1718
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
1819
}
@@ -26,4 +27,4 @@ test {
2627
testLogging {
2728
events('failed')
2829
}
29-
}
30+
}

src/main/client/src/Lobby.jsx

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,20 @@ import {
88
import {
99
useNavigate,
1010
} from "react-router-dom"
11+
import toast from "react-hot-toast"
12+
import {
13+
Form,
14+
} from "./component/Form.jsx"
15+
import {
16+
Input,
17+
} from "./component/Input.jsx"
1118
import {
1219
Button,
1320
} from "./component/Button.jsx"
1421
import {
1522
base,
1623
StompContext,
24+
tfetch,
1725
} from "./util.js"
1826
import {
1927
LobbyPanel,
@@ -25,6 +33,8 @@ import {
2533

2634
export function Lobby() {
2735
let [matchRequested, setMatchRequested] = useState(false)
36+
let [isNewGameOpen, setNewGameOpen] = useState(false)
37+
let [openGames, setOpenGames] = useState([])
2838
let stompClient = useContext(StompContext)
2939
let navigate = useNavigate()
3040
let auth = useAuthStore(state => state.auth)
@@ -45,6 +55,10 @@ export function Lobby() {
4555
setMatchRequested(true)
4656
}
4757
})
58+
let sub3 = stompClient.subscribe("/topic/lobby/open", (message) => {
59+
let r = JSON.parse(message.body)
60+
setOpenGames(r.games)
61+
})
4862
stompClient.publish({
4963
destination: "/app/lobby/hello",
5064
body: JSON.stringify({
@@ -53,8 +67,23 @@ export function Lobby() {
5367
return () => {
5468
sub1.unsubscribe()
5569
sub2.unsubscribe()
70+
sub3.unsubscribe()
5671
}
5772
}, [setInit, setMatchRequested, auth, initialized, stompClient, navigate])
73+
let onNewGame = useCallback(async (d) => {
74+
try {
75+
await tfetch("/api/create", {
76+
method: "POST",
77+
headers: {
78+
"Authorization": "Bearer " + auth.token,
79+
"Content-Type": "application/json",
80+
},
81+
body: JSON.stringify(d),
82+
})
83+
} catch (e) {
84+
toast.error(e.message)
85+
}
86+
}, [auth.token])
5887
let matchRequest = useCallback((editMode) => {
5988
stompClient.publish({
6089
destination: "/app/lobby/match",
@@ -85,6 +114,27 @@ export function Lobby() {
85114
Find match
86115
</Button>
87116
</div>
117+
<div className="mt-2">
118+
<Button
119+
onClick={() => setNewGameOpen(!isNewGameOpen)}>
120+
New Game
121+
</Button>
122+
</div>
123+
{isNewGameOpen && (
124+
<Form className="mt-2" onSubmit={onNewGame}>
125+
<Input name="dim" defaultValue="9"/>
126+
<Input name="handicap" defaultValue="0"/>
127+
<Button
128+
type="submit">
129+
OK
130+
</Button>
131+
</Form>
132+
)}
133+
<div className="mt-2">
134+
{openGames.map((game) => (
135+
<div key={game.id}>{game.user.name}, {game.dim}x{game.dim}</div>
136+
))}
137+
</div>
88138
<LobbyPanel />
89139
</div>
90140
)

src/main/client/src/Login.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
} from "react-router-dom"
1313
import toast, {
1414
Toaster,
15-
} from "react-hot-toast";
15+
} from "react-hot-toast"
1616
import {
1717
base,
1818
tfetch,
@@ -54,7 +54,7 @@ export function Login() {
5454
Join
5555
</Button>
5656
</Form>
57-
<Toaster position="top-right"/>
57+
<Toaster position="top-right" />
5858
</>
5959
)
6060
}

src/main/client/src/Router.jsx

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ import {
1111
Outlet,
1212
Navigate,
1313
} from "react-router-dom"
14+
import {
15+
Toaster,
16+
} from "react-hot-toast"
1417
import {
1518
useAuthStore,
1619
} from "./store.js"
@@ -34,11 +37,18 @@ export const Router = createBrowserRouter(
3437
<Route
3538
element={<WithConnection />}>
3639
<Route
37-
path={base + "/lobby"}
38-
element={<Lobby />} />
39-
<Route
40-
path={base + "/game/:gameId"}
41-
element={<Game />} />
40+
element={
41+
<div>
42+
<Outlet />
43+
<Toaster position="top-right" />
44+
</div>}>
45+
<Route
46+
path={base + "/lobby"}
47+
element={<Lobby />} />
48+
<Route
49+
path={base + "/game/:gameId"}
50+
element={<Game />} />
51+
</Route>
4252
</Route>
4353
<Route
4454
path={base + "/login"}

src/main/java/com/bernd/GameController.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
import com.bernd.model.OpenGame;
77
import com.bernd.model.OpenGameList;
88
import com.bernd.util.RandomString;
9+
import java.util.Objects;
910
import org.springframework.http.ResponseEntity;
1011
import org.springframework.messaging.core.MessageSendingOperations;
1112
import org.springframework.messaging.handler.annotation.MessageMapping;
13+
import org.springframework.security.core.context.SecurityContextHolder;
1214
import org.springframework.stereotype.Controller;
1315
import org.springframework.web.bind.annotation.PostMapping;
1416
import org.springframework.web.bind.annotation.RequestBody;
@@ -47,7 +49,8 @@ public void action(Move move) {
4749
@PostMapping(value = "/api/create", consumes = "application/json")
4850
@ResponseBody
4951
public ResponseEntity<?> newGame(@RequestBody OpenGame openGame) {
50-
openGames.put(RandomString.get(), openGame);
52+
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
53+
openGames.put(RandomString.get(), openGame.withUser(Objects.toString(principal, "")));
5154
operations.convertAndSend("/topic/lobby/open",
5255
new OpenGameList(openGames.games()));
5356
return ResponseEntity.ok().build();

src/main/java/com/bernd/LobbyController.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.bernd.model.Game;
44
import com.bernd.model.MatchRequest;
5+
import com.bernd.model.OpenGameList;
56
import com.bernd.model.Status;
67
import com.bernd.model.User;
78
import com.bernd.model.UserList;
@@ -19,22 +20,27 @@ public class LobbyController {
1920
private final MessageSendingOperations<String> operations;
2021
private final LobbyUsers lobbyUsers;
2122
private final Games games;
23+
private final OpenGames openGames;
2224
private User lookingForMatch;
2325

2426
LobbyController(
2527
MessageSendingOperations<String> operations,
2628
LobbyUsers lobbyUsers,
29+
OpenGames openGames,
2730
Games games) {
2831
this.operations = operations;
2932
this.lobbyUsers = lobbyUsers;
3033
this.games = games;
34+
this.openGames = openGames;
3135
}
3236

3337
@MessageMapping("/lobby/hello")
3438
public void lobbyJoinedAction(Principal principal) {
3539
lobbyUsers.add(principal);
3640
operations.convertAndSend("/topic/lobby/users",
3741
new UserList(lobbyUsers.users()));
42+
operations.convertAndSend("/topic/lobby/open",
43+
new OpenGameList(openGames.games()));
3844
}
3945

4046
@MessageMapping("/lobby/match")

src/main/java/com/bernd/OpenGames.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ OpenGame get(String id) {
1515
}
1616

1717
OpenGame put(String id, OpenGame game) {
18-
map.put(game.id(), game);
18+
map.put(game.id(), game.withId(id));
1919
return game;
2020
}
2121

src/main/java/com/bernd/UserInterceptor.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,11 @@ public Message<?> preSend(Message<?> message, MessageChannel channel) {
3838
}
3939
DecodedJWT jwt = verifier.verify(tokens.get(0));
4040
Claim name = jwt.getClaim("name");
41-
if (name == null) {
41+
if (name.asString() == null) {
4242
return null;
4343
}
4444
accessor.setUser(new StompUser(name.asString()));
4545
}
4646
return message;
4747
}
48-
}
48+
}

src/main/java/com/bernd/model/OpenGame.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,8 @@ public record OpenGame(
99
public OpenGame withId(String id) {
1010
return new OpenGame(id, user, dim, handicap);
1111
}
12+
13+
public OpenGame withUser(String name) {
14+
return new OpenGame(id, new User(name), dim, handicap);
15+
}
1216
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package com.bernd.util;
2+
3+
import com.auth0.jwt.JWT;
4+
import com.auth0.jwt.JWTVerifier;
5+
import com.auth0.jwt.algorithms.Algorithm;
6+
import com.auth0.jwt.interfaces.DecodedJWT;
7+
import jakarta.servlet.FilterChain;
8+
import jakarta.servlet.ServletException;
9+
import jakarta.servlet.http.HttpServletRequest;
10+
import jakarta.servlet.http.HttpServletResponse;
11+
import java.io.IOException;
12+
import java.util.List;
13+
import org.springframework.core.env.Environment;
14+
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
15+
import org.springframework.security.core.context.SecurityContextHolder;
16+
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
17+
import org.springframework.stereotype.Component;
18+
import org.springframework.web.filter.OncePerRequestFilter;
19+
20+
@Component
21+
public class AuthFilter extends OncePerRequestFilter {
22+
23+
private final JWTVerifier verifier;
24+
25+
public AuthFilter(Environment environment) {
26+
Algorithm algorithm = Algorithm.HMAC512(environment.getProperty("jwt.secret.key"));
27+
this.verifier = JWT.require(algorithm).build();
28+
}
29+
30+
@Override
31+
protected void doFilterInternal(
32+
HttpServletRequest request,
33+
HttpServletResponse response,
34+
FilterChain filterChain) throws ServletException, IOException {
35+
String username = getUsername(request);
36+
if (username == null) {
37+
filterChain.doFilter(request, response);
38+
return;
39+
}
40+
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, null, List.of());
41+
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
42+
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
43+
filterChain.doFilter(request, response);
44+
SecurityContextHolder.getContext().setAuthentication(null);
45+
}
46+
47+
private String getUsername(HttpServletRequest request) {
48+
String authHeader = request.getHeader("Authorization");
49+
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
50+
return null;
51+
}
52+
String token = authHeader.substring(7);
53+
DecodedJWT jwt = verifier.verify(token);
54+
return jwt.getClaim("name").asString();
55+
}
56+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.bernd.util;
2+
3+
import org.springframework.boot.autoconfigure.security.SecurityProperties;
4+
import org.springframework.context.annotation.Bean;
5+
import org.springframework.context.annotation.Configuration;
6+
import org.springframework.core.annotation.Order;
7+
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
8+
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
9+
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
10+
import org.springframework.security.config.http.SessionCreationPolicy;
11+
import org.springframework.security.web.SecurityFilterChain;
12+
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
13+
14+
@Configuration
15+
@EnableWebSecurity
16+
@Order(SecurityProperties.BASIC_AUTH_ORDER - 10)
17+
public class SecurityConfig {
18+
19+
private final AuthFilter authFilter;
20+
21+
public SecurityConfig(AuthFilter authFilter) {
22+
this.authFilter = authFilter;
23+
}
24+
25+
// https://www.springboottutorial.com/securing-rest-services-with-spring-boot-starter-security
26+
@Bean
27+
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
28+
http.authorizeHttpRequests(auth -> auth.anyRequest().permitAll());
29+
http.csrf(AbstractHttpConfigurer::disable);
30+
http.addFilterBefore(authFilter, UsernamePasswordAuthenticationFilter.class);
31+
http.sessionManagement(sessionConfigurer -> {
32+
sessionConfigurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
33+
});
34+
return http.build();
35+
}
36+
}

0 commit comments

Comments
 (0)